Body of View is not rerendered - ios

I am making QuizApp. Currently I have viewModel in which questions are fetched and stored in #Published array.
class HomeViewModel: ObservableObject {
let repository: QuestionRepository
#Published var questions: [Question] = []
#Published var isLoading: Bool = true
private var cancellables: Set<AnyCancellable> = .init()
init(repository: QuestionRepository){
self.repository = repository
getQuestions()
}
private func getQuestions(){
repository
.getQuestions()
.receive(on: RunLoop.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] questions in
self?.isLoading = false
self?.questions = questions
}
)
.store(in: &cancellables)
}
func updateQuestions(){
questions.removeFirst()
if questions.count < 2 {
getQuestions()
}
}
}
In QuestionContainerView HomeViewModel is created as #StateObject and from it, first data from questions array is used and passed to QuestionView.
#StateObject private var viewModel: HomeViewModel = HomeViewModel(repository: QuestionRepositoryImpl())
var body: some View {
if viewModel.isLoading {
ProgressView()
} else {
VStack(alignment: .leading, spacing: 16) {
if let question = viewModel.questions.first {
QuestionView(question: question){
viewModel.updateQuestions()
}
} else {
Text("No more questions")
.font(.title2)
}
}
.padding()
}
}
QuestionView has two properties, Question and showNextQuestion callback.
let question: Question
let showNextQuestion: () -> Void
And when some button is pressed in that view, callBack is called after 2.5s and after that viewModel function updateQuestions is called.
struct QuestionView: View {
let question: Question
let showNextQuestion: () -> Void
#State private var showCorrectAnswer: Bool = false
#State private var timeRemaining = 10
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("\(timeRemaining)")
.font(.title2)
Text(question.question)
.font(.title2)
.padding()
ForEach(question.allAnswers, id: \.self){ answer in
Button(
action: {
showCorrectAnswer.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
showNextQuestion()
}
},
label: {
Text(answer)
.font(.title2)
.padding()
.background(getBackgroundColor(answer: answer))
.clipShape(Capsule())
}
)
}
Spacer()
}
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
} else {
showNextQuestion()
}
}
}
My idea was to pass first item from viewModel array to QuestionView and after some Button action in QuestionView I wanted to remove firstItem from array and pass next firstItem.
But problem is that QuestionView is not updated (it is not rerendered) and it contains some data from past item - I added timer in QuestionView which is counting down and when question is changed, timer value is still same as for before question, it is not reseted.
I thought that marking viewModel array property with #Published will trigger whole QuestionContainerView render with new viewModel first item from array, but it is not updated as I wanted.

There are several mistakes in the SwiftUI code, one or all could contribute to the problem, here are the ones I noticed:
We don't use view model objects in SwiftUI for view data, that's the job of the View struct and property wrappers.
When ObservableObject is being used for model data, it's usually a singleton (one for the app and another for previews) and passed in as environmentObject. We don't usually use the reference version of #State, i.e. #StateObject for holding the model since we don't want model lifetime tied to any view on screen, it has to be tied to the app executable's lifetime. Also, #StateObject are disabled for previews since usually those are used for network downloads.
In an ObservableObject we .assign(to: &$propertyName) the end of the pipeline to an #Published var, we don't use sink or need cancellables in this case. This ties the pipeline's lifetime to the object's, if you use sink you need to cancel it yourself when the object de-inits (Not required for singletons but it's good to learn the pattern).
Since your timer is a let it will be lost every time the QuestionView is re-init, to fix it needs to be #State.
ForEach is a View not a for loop. You have either supply Identifiable data or an id param, you can't use id:\.self for dynamic data or it'll crash when it changes.

Related

How to refresh Core Data array when user enters new view with SwiftUI?

I have 3 views. Content View, TrainingView and TrainingList View. I want to list exercises from Core Data but also I want to make some changes without changing data.
In ContentView; I am trying to fetch data with CoreData
struct ContentView: View {
// MARK: - PROPERTY
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Training.timestamp, ascending: false)],
animation: .default)
private var trainings: FetchedResults<Training>
#State private var showingAddProgram: Bool = false
// FETCHING DATA
// MARK: - FUNCTION
// MARK: - BODY
var body: some View {
NavigationView {
Group {
VStack {
HStack {
Text("Your Programs")
Spacer()
Button(action: {
self.showingAddProgram.toggle()
}) {
Image(systemName: "plus")
}
.sheet(isPresented: $showingAddProgram) {
AddProgramView()
}
} //: HSTACK
.padding()
List {
ForEach(trainings) { training in
TrainingListView(training: training)
}
} //: LIST
Spacer()
} //: VSTACK
} //: GROUP
.navigationTitle("Good Morning")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
print("test")
}) {
Image(systemName: "key")
}
}
} //: TOOLBAR
.onAppear() {
}
} //: NAVIGATION
}
private func showId(training: Training) {
guard let id = training.id else { return }
print(id)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
In TrainingView; I am getting exercises as a array list and I am pushing into to TrainingListView.
import SwiftUI
struct TrainingView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var training: Training
#State var exercises: [Exercise]
#State var tempExercises: [Exercise] = [Exercise]()
#State var timeRemaining = 0
#State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var isTimerOn = false
var body: some View {
VStack {
HStack {
Text("\(training.name ?? "")")
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Finish")
}
}
.padding()
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 250, height: 250)
Circle()
.fill(Color.white)
.frame(width: 240, height: 240)
Text("\(timeRemaining)s")
.font(.system(size: 100))
.fontWeight(.ultraLight)
.onReceive(timer) { _ in
if isTimerOn {
if timeRemaining > 0 {
timeRemaining -= 1
} else {
isTimerOn.toggle()
stopTimer()
removeExercise()
}
}
}
}
Button(action: {
startResting()
}) {
if isTimerOn {
Text("CANCEL")
} else {
Text("GIVE A BREAK")
}
}
Spacer()
ExerciseListView(exercises: $tempExercises)
}
.navigationBarHidden(true)
.onAppear() {
updateBigTimer()
}
}
private func startResting() {
tempExercises = exercises
if let currentExercise: Exercise = tempExercises.first {
timeRemaining = Int(currentExercise.rest)
startTimer()
isTimerOn.toggle()
}
}
private func removeExercise() {
if let currentExercise: Exercise = tempExercises.first {
if Int(currentExercise.rep) == 1 {
let index = tempExercises.firstIndex(of: currentExercise) ?? 0
tempExercises.remove(at: index)
} else if Int(currentExercise.rep) > 1 {
currentExercise.rep -= 1
let index = tempExercises.firstIndex(of: currentExercise) ?? 0
tempExercises.remove(at: index)
tempExercises.insert(currentExercise, at: index)
}
updateBigTimer()
}
}
private func updateBigTimer() {
timeRemaining = Int(tempExercises.first?.rest ?? 0)
}
private func stopTimer() {
timer.upstream.connect().cancel()
}
private func startTimer() {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
}
struct TrainingView_Previews: PreviewProvider {
static var previews: some View {
TrainingView(training: Training(), exercises: [Exercise]())
}
}
In TrainingListView; I am listing all exercises.
struct TrainingListView: View {
#ObservedObject var training: Training
#Environment(\.managedObjectContext) private var managedObjectContext
var body: some View {
NavigationLink(destination: TrainingView(training: training, exercises: training.exercises?.toArray() ?? [Exercise]())) {
HStack {
Text("\(training.name ?? "")")
Text("\(training.exercises?.count ?? 0) exercises")
}
}
}
}
Also, I am adding video: https://twitter.com/huseyiniyibas/status/1388571724346793986
What I want to do is, when user taps any Training Exercises List should refreshed. It should be x5 again like in the beginning.
I had a hard time understanding your question but I guess I got the idea.
My understanding is this:
You want to store the rep count in the Core Data. (Under Training > Exercises)
You want to count down the reps one by one as the user completes the exercise.
But you don't want to change the original rep count stored in the Core Data.
I didn't run your code since I didn't want to recreate all the models and Core Data files. I guess I've spotted the problem. Here I'll explain how you can solve it:
The Core Data models are classes (reference types). When you pass around the classes (as you do in your code) and change their properties, you change the original data. In your case, you don't want that.
(Btw, being a reference type is a very useful and powerful property of classes. Structs and enums are value types, i.e. they are copied when passed around. The original data is unchanged.)
You have several options to solve your problem:
Just generate a different struct (something like ExerciseDisplay) from Exercise, and pass ExerciseDisplay to TrainingView.
You can write an extension to Exercise and "copy" the model before passing it to TrainingView. For this you'll need to implement the NSCopying protocol.
extension Exercise: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return Exercise(...)
}
}
But before doing this I guess you'll need to change the Codegen to Manual/None of your entry in your .xcdatamodeld file. This is needed when you want to create the attributes manually. I'm not exactly sure how you can implement NSCopying for a CoreDate model, but it's certainly doable.
The first approach is easier but kinda ugly. The second is more versatile and elegant, but it's also more advanced. Just try the first approach first and move to the second once you feel confident.
Update:
This is briefly how you can implement the 1st approach:
struct ExerciseDisplay: Identifiable, Equatable {
public let id = UUID()
public let name: String
public var rep: Int
public let rest: Int
}
struct TrainingView: View {
// Other properties and states etc.
let training: Training
#State var exercises: [ExerciseDisplay] = []
init(training: Training) {
self.training = training
}
var body: some View {
VStack {
// Views
}
.onAppear() {
let stored: [Exercise] = training.exercises?.toArray() ?? []
self.exercises = stored.map { ExerciseDisplay(name: $0.name ?? "", rep: Int($0.rep), rest: Int($0.rest)) }
}
}
}

SwiftUI Combine Debounce TextField

I have a SwiftUI app with SwiftUI App life cycle. I'm trying to setup a standard way to add
typing debounce to TextFields. Ideally, I'd like to create my own TextField modifier that
can easily be applied to views that have many textfields to edit. I've tried a bunch of
ways to do this but I must be missing something fundamental. Here's one example. This
does not work:
struct ContentView: View {
#State private var searchText = ""
var body: some View {
VStack {
Text("You entered: \(searchText)")
.padding()
TextField("Enter Something", text: $searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
.onChange(of: searchText, perform: { _ in
var subscriptions = Set<AnyCancellable>()
let pub = PassthroughSubject<String, Never>()
pub
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.collect()
.sink(receiveValue: { t in
self.searchText = t.first ?? "nothing"
} )
.store(in: &subscriptions)
})
}
}
}
Any guidance would be appreciated. Xcode 12.4, iOS 14.4
I think you'll have to keep two variables: one for the text in the field as the user is typing and one for the debounced text. Otherwise, the user wouldn't see the typing coming in in real-time, which I'm assuming isn't the behavior you want. I'm guessing this is probably for the more standard use case of, say, performing a data fetch once the user has paused their typing.
I like ObservableObjects and Combine to manage this sort of thing:
class TextFieldObserver : ObservableObject {
#Published var debouncedText = ""
#Published var searchText = ""
private var subscriptions = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] t in
self?.debouncedText = t
} )
.store(in: &subscriptions)
}
}
struct ContentView: View {
#StateObject var textObserver = TextFieldObserver()
#State var customText = ""
var body: some View {
VStack {
Text("You entered: \(textObserver.debouncedText)")
.padding()
TextField("Enter Something", text: $textObserver.searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
Divider()
Text(customText)
TextFieldWithDebounce(debouncedText: $customText)
}
}
}
struct TextFieldWithDebounce : View {
#Binding var debouncedText : String
#StateObject private var textObserver = TextFieldObserver()
var body: some View {
VStack {
TextField("Enter Something", text: $textObserver.searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
}.onReceive(textObserver.$debouncedText) { (val) in
debouncedText = val
}
}
}
I included two examples -- the top, where the container view (ContentView) owns the ObservableObject and the bottom, where it's made into a more-reusable component.
A little simplified version of text debouncer from #jnpdx
Note that .assign(to: &$debouncedText) doesn't create a reference cycle and manages subscription for you automatically
class TextFieldObserver : ObservableObject {
#Published var debouncedText = ""
#Published var searchText = ""
init(delay: DispatchQueue.SchedulerTimeType.Stride) {
$searchText
.debounce(for: delay, scheduler: DispatchQueue.main)
.assign(to: &$debouncedText)
}
}
If you are not able to use an ObservableObject (ie, if your view is driven by a state machine, or you are passing the input results to a delegate, or are simply publishing the input), there is a way to accomplish the debounce using only view code. This is done by forwarding text changes to a local Publisher, then debouncing the output of that Publisher.
struct SomeView: View {
#State var searchText: String = ""
let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
TextField("Search", text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
print(debouncedSearchText)
}
}
}
Or if broadcasting the changes:
struct DebouncedSearchField: View {
#Binding var debouncedSearchText: String
#State private var searchText: String = ""
private let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
TextField("Search", text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
self.debouncedSearchText = debouncedSearchText
}
}
}
However, if you have the choice, it may be more "correct" to go with the ObservableObject approach.
Tunous on GitHub added a debounce extension to onChange recently. https://github.com/Tunous/DebouncedOnChange that is super simple to use. Instead of adding .onChange(of: value) {newValue in doThis(with: newValue) } you can add .onChange(of: value, debounceTime: 0.8 /sec/ ) {newValue in doThis(with: newValue) }
He sets up a Task that sleeps for the debounceTime but it is cancelled and reset on every change to value. The view modifier he created uses a State var debounceTask. It occurred to me that this task could be a Binding instead and shared amount multiple onChange view modifiers allowing many textfields to be modified on the same debounce. This way if you programmatically change a bunch of text fields using the same debounceTask only one call to the action is made, which is often what one wants to do. Here is the code with a simple example.
//
// Debounce.swift
//
// Created by Joseph Levy on 7/11/22.
// Based on https://github.com/Tunous/DebouncedOnChange
import SwiftUI
import Combine
extension View {
/// Adds a modifier for this view that fires an action only when a time interval in seconds represented by
/// `debounceTime` elapses between value changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action /// will be scheduled to run after that time passes again. This mean that the action will only execute
/// after changes to the value /// stay unmodified for the specified `debounceTime` in seconds.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - debounceTime: The time in seconds to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
perform action: #escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedChangeViewModifier(trigger: value, debounceTime: debounceTime, action: action))
}
/// Same as above but adds before action
/// - debounceTask: The common task for multiple Values, but can be set to a different action for each change
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
task: Binding< Task<Void,Never>? >,
perform action: #escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedTaskBindingChangeViewModifier(trigger: value, debounceTime: debounceTime, debouncedTask: task, action: action))
}
}
private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
let action: (Value) -> Void
#State private var debouncedTask: Task<Void,Never>?
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { #MainActor in
action(value)
}
}
}
}
private struct DebouncedTaskBindingChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
#Binding var debouncedTask: Task<Void,Never>?
let action: (Value) -> Void
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { #MainActor in
action(value)
}
}
}
}
extension Task {
/// Asynchronously runs the given `operation` in its own task after the specified number of `seconds`.
///
/// The operation will be executed after specified number of `seconds` passes. You can cancel the task earlier
/// for the operation to be skipped.
///
/// - Parameters:
/// - time: Delay time in seconds.
/// - operation: The operation to execute.
/// - Returns: Handle to the task which can be cancelled.
#discardableResult
public static func delayed(
seconds: TimeInterval,
operation: #escaping #Sendable () async -> Void
) -> Self where Success == Void, Failure == Never {
Self {
do {
try await Task<Never, Never>.sleep(nanoseconds: UInt64(seconds * 1e9))
await operation()
} catch {}
}
}
}
// MultiTextFields is an example
// when field1, 2 or 3 change the number times is incremented by one, one second later
// when field changes the three other fields are changed too but the increment task only
// runs once because they share the same debounceTask
struct MultiTextFields: View {
#State var debounceTask: Task<Void,Never>?
#State var field: String = ""
#State var field1: String = ""
#State var field2: String = ""
#State var field3: String = ""
#State var times: Int = 0
var body: some View {
VStack {
HStack {
TextField("Field", text: $field).padding()
.onChange(of: field, debounceTime: 1) { newField in
field1 = newField
field2 = newField
field3 = newField
}
Text(field+" \(times)").padding()
}
Divider()
HStack {
TextField("Field1", text: $field1).padding()
.onChange(of: field1, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field1+" \(times)").padding()
}
HStack {
TextField("Field2", text: $field2).padding()
.onChange(of: field2, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field2+" \(times)").padding()
}
HStack {
TextField("Field3", text: $field3).padding()
.onChange(of: field3, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field3+" \(times)").padding()
}
}
}
}
struct View_Previews: PreviewProvider {
static var previews: some View {
MultiTextFields()
}
}
I haven't tried the shared debounceTask binding using an ObservedObject or StateObject, just a State var as yet. If anyone tries that please post the result.

SwiftUI Lists + Firebase Firestore, fetching data and then unfetching? (Bug)

I'm not sure what the problem is with my project. Basically, I have a pretty typical data structure: I'm using Firestore for a real-time doc based database, and I have a bunch of different collections and docs and fields. Just very simple, nothing crazy.
I have some model classes, and then some ViewModels to fetch data, filter, add documents to Firestore, and so on.
The app itself is almost 100% SwiftUI, and I'd like to keep it that way, just a challenge for my own development. I've hit a bit of a wall though.
In the app, I have a series of Views with NavigationView Lists that I pass small pieces of data to as you navigate. An example (this is for educational institutions) might be List of Schools > List of Classes > List of Students in Class > List of Grades for Student. Basically the perfect use for a navigation view and a bunch of lists.
The bug:
When I move from one list to the next in the stack, I fetch the firestore data, which loads for a second (enough that the list populates), and then "unloads" back to nothing. Here is some code where this happens (I've cleaned it up to make it as simple as possible):
struct ClassListView: View {
let schoolCode2: String
let schoolName2: String
#ObservedObject private var viewModelThree = ClassViewModel()
var body: some View {
VStack{
List{
if viewModelThree.classes.count > 0{
ForEach(self.viewModelThree.classes) { ownedClass in
NavigationLink(destination: StudentListView()){
Text(ownedClass.className)
}
}
} else {
Text("No Classes Found for \(schoolName2)")
}
}
}
.navigationBarTitle(schoolName2)
.onAppear(){
print("appeared")
self.viewModelThree.fetchData(self.schoolCode2)
}
}
}
So that's the ClassListView that I keep having issues with. For debugging, I added the else Text("No Classes Found") line and it does in fact show. So basically, view loads (this is all in a Nav view from a parent), it fetches the data, which is shown for a second (list populates) and then unloads that data for some reason, leaving me with just the "No classes found".
For more context, here is the code for the ClassViewModel (maybe that's where I'm going wrong?):
struct Classes: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var schoolCode: String
var className: String
}
enum ClassesCodingKeys: String, CodingKey {
case id
case schoolCode
case className
}
class ClassViewModel: ObservableObject {
#Published var classes = [Classes]()
private var db = Firestore.firestore()
func fetchData(_ schoolCode: String) {
db.collection("testClasses")
.order(by: "className")
.whereField("schoolCode", isEqualTo: schoolCode)
.addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no docs found")
return
}
self.classes = documents.compactMap{ queryDocumentSnapshot -> Classes? in
return try? queryDocumentSnapshot.data(as: Classes.self)
}
}
}
func addClass(currentClass: Classes){
do {
let _ = try db.collection("testClasses").addDocument(from: currentClass)
}
catch {
print(error)
}
}
}
Most relevant bit is the fetchData() function above.
Maybe the problem is in the view BEFORE this (the parent view?). Here it is:
struct SchoolUserListView: View {
#State private var userId: String?
#EnvironmentObject var session: SessionStore
#ObservedObject private var viewModel = UserTeacherViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var counter = 0
#State private var showingAddSchool: Bool = false
#Environment(\.presentationMode) var presentationMode
func getUser() {
session.listen()
}
var body: some View {
VStack{
List{
if viewModel.teachers.count > 0{
ForEach(viewModel.teachers) { ownedClass in
NavigationLink(destination: ClassListView(schoolName2: ownedClass.schoolName, schoolCode2: ownedClass.schoolCode)){
Text(ownedClass.schoolName)
}
}
} else {
Button(action:{
self.presentationMode.wrappedValue.dismiss()
}){
Text("You have no schools, why not add one?")
}
}
}
.navigationBarTitle("Your Schools")
}
.onAppear(){
self.getUser()
self.userId = self.session.session?.uid
}
.onReceive(timer){ time in
if self.counter == 1 {
self.timer.upstream.connect().cancel()
} else {
print("fetching")
self.viewModel.fetchData(self.userId!)
}
self.counter += 1
}
}
}
And the FURTHER Parent to that View (and in fact the starting view of the app):
struct StartingView: View {
#EnvironmentObject var session: SessionStore
func getUser() {
session.listen()
}
var body: some View {
NavigationView{
VStack{
Text("Welcome!")
Text("Select an option below")
Group{
NavigationLink(destination:
SchoolUserListView()
){
Text("Your Schools")
}
NavigationLink(destination:
SchoolCodeAddView()
){
Text("Add a School")
}
Spacer()
Button(action:{
self.session.signOut()
}){
Text("Sign Out")
}
}
}
}
.onAppear(){
self.getUser()
}
}
}
I know that is a lot, but just so everyone has the order of parents:
StartingView > SchoolUserListView > ClassListView(BUG occurs here)
By the way, SchoolUserListView uses a fetchData method just like ClassListView(where the bug is) and outputs it to a foreach list, same thing, but NO problems? I'm just not sure what the issue is, and any help is greatly appreciated!
Here is a video of the issue:
https://imgur.com/a/kYtow6G
Fixed it!
The issue was the EnvironmentObject for presentationmode being passed in the navigation view. That seems to cause a LOT of undesired behavior.
Be careful passing presentationMode as it seems to cause data to reload (because it is being updated).

Custom SwiftUI view with a two-way binding

I'm struggling to implement a custom view which can take Binding as an argument and implement two-way updates of that value.
So basically I'm implementing my custom slider and want its initializer to be like this:
MySlider(value: <Binding<Float>)
What I'm struggling with:
How do I subscribe to remote updates of the binding value so that I can update the view's state?
Is there any nice way to bind a Binding with #State property?
Here's my current implementation so far which is not perfect.
struct MySlider: View {
#Binding var selection: Float?
#State private var selectedValue: Float?
init(selection: Binding<Float?>) {
self._selection = selection
// https://stackoverflow.com/a/58137096
_selectedValue = State(wrappedValue: selection.wrappedValue)
}
var body: some View {
HStack(spacing: 3) {
ForEach(someValues) { (v) in
Item(value: v,
isSelected: v == self.selection)
.onTapGesture {
// No idea how to do that other way so I don't have to set it twice
self.selection = v
self.selectedValue = v
}
}
}
}
}
Edit 1:
I suppose my problem is that the underlying model object comes from Core Data and wasn't owned by any SwiftUI view which would observe its changes. The model object was owned by the UIKit ViewController and I was passing only a Binding to the SwiftUI view which is not enough.
My solution now is to pass the model object also to the SwiftUI View so that it can marked it as an #ObservedObject.
struct MySlider<T>: View where T: ObservableObject {
#ObservedObject var object: T
#Binding var selection: Float?
var body: some View {
return ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 3) {
ForEach(values) { (v) in
Item(value: v,
isSelected: v == self.selection)
.onTapGesture {
self.selection = v
}
}
}
}
}
}
The definition of #Binding is essentially a two-way connection to an underlying data item, such a a #State variable that's owned by another View. As stated by Apple:
Use a binding to create a two-way connection between a view and its underlying model.
If it's a binding, SwiftUI will already update its views automatically if the value changes; so (in answer to your first question), you don't need to do any sort of subscriptions to update your custom view - that will happen automatically.
Similarly (regarding your second question), because the binding is the state from another view, you shouldn't be declaring it as a state for your view as well, and I also don't believe it's possible. State is something that should be a purely internal value to your view (Apple strongly recommend that all #State properties are declared private).
This all ties back to the 'Single Source of Truth' concept Apple stressed when unveiling SwiftUI: The parent view where that binding's already #State is what owns the information, so it's not something your view should also declare as a state.
For your code, I think all you need to do is take out that second state property, because it's not required. Just make sure the binding you pass is a #State property in whatever parent view owns your custom view, and pass it in using the $ syntax to create that binding. This article covers the idea in more detail if you need.
struct MySlider: View {
#Binding var selection: Float?
init(selection: Binding<Float?>) {
self._selection = selection
}
var body: some View {
HStack(spacing: 3) {
ForEach(someValues) { (v) in
Item(value: v, isSelected: v == self.selection)
.onTapGesture {
self.selection = v
}
}
}
}
}
My answer might be a little late, but i stumbled across a similar problem. So here is how i solved it.
struct Item : Identifiable {
var id : Int
var isSelected : Bool
}
class MySliderObserver : ObservableObject {
#Published var values = [Item]()
init() {
values.append(Item(id: 0, isSelected: false))
values.append(Item(id: 1, isSelected: false))
values.append(Item(id: 2, isSelected: false))
}
func toggleSelectForItem(id:Int) -> Void {
guard let index = values.firstIndex(where: { $0.id == id }) else {
return
}
var item = values[index]
item.isSelected.toggle()
values[index] = item
}
}
struct MySlider: View {
#EnvironmentObject var observer : MySliderObserver
var body: some View {
return ScrollView(.horizontal, showsIndicators: false) {
VStack(spacing: 3) {
ForEach(self.observer.values) { v in
Text("Selected: \(v.isSelected ? "true" : "false")").onTapGesture {
self.observer.toggleSelectForItem(id: v.id)
}
}
}
}
}
}
and in the SceneDelegate you need to set an EnvironmentObject like:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = MySlider()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(MySliderObserver()))
self.window = window
window.makeKeyAndVisible()
}
}

SwiftUI: How to get continuous updates from Slider

I'm experimenting with SwiftUI and the Slider control like this:
struct MyView: View {
#State private var value = 0.5
var body: some View {
Slider(value: $value) { pressed in
}
}
}
I'm trying to get continuous updates from the Slider as the user drags it, however it appears that it only updates the value at the end of the value change.
Anyone played with this? know how to get a SwiftUI Slider to issue a stream of value changes? Combine perhaps?
In SwiftUI, you can bind UI elements such as slider to properties in your data model and implement your business logic there.
For example, to get continuous slider updates:
import SwiftUI
import Combine
final class SliderData: BindableObject {
let didChange = PassthroughSubject<SliderData,Never>()
var sliderValue: Float = 0 {
willSet {
print(newValue)
didChange.send(self)
}
}
}
struct ContentView : View {
#EnvironmentObject var sliderData: SliderData
var body: some View {
Slider(value: $sliderData.sliderValue)
}
}
Note that to have your scene use the data model object, you need to update your window.rootViewController to something like below inside SceneDelegate class, otherwise the app crashes.
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(SliderData()))
After much playing around I ended up with the following code. It's a little cut down to keep the answer short, but here goes. There was a couple of things I needed:
To read value changes from the slider and round them to the nearest integer before setting an external binding.
To set a localized hint value based on the integer.
struct AspectSlider: View {
// The first part of the hint text localization key.
private let hintKey: String
// An external integer binding to be set when the rounded value of the slider
changes to a different integer.
private let value: Binding<Int>
// A local binding that is used to track the value of the slider.
#State var sliderValue: Double = 0.0
init(value: Binding<Int>, hintKey: String) {
self.value = value
self.hintKey = hintKey
}
var body: some View {
VStack(alignment: .trailing) {
// The localized text hint built from the hint key and the rounded slider value.
Text(LocalizedStringKey("\(hintKey).\(self.value.value)"))
HStack {
Text(LocalizedStringKey(self.hintKey))
Slider(value: Binding<Double>(
getValue: { self.$sliderValue.value },
setValue: { self.sliderChanged(toValue: $0) }
),
through: 4.0) { if !$0 { self.slideEnded() } }
}
}
}
private func slideEnded() {
print("Moving slider to nearest whole value")
self.sliderValue = self.sliderValue.rounded()
}
private func sliderChanged(toValue value: Double) {
$sliderValue.value = value
let roundedValue = Int(value.rounded())
if roundedValue == self.value.value {
return
}
print("Updating value")
self.value.value = roundedValue
}
}
We can go without custom bindings, custom inits, ObservableObjects, PassthroughSubjects, #Published and other complications. Slider has .onChange(of: perform:) modifier which is perfect for this case.
This answer can be rewritten as follows:
struct AspectSlider2: View {
#Binding var value: Int
let hintKey: String
#State private var sliderValue: Double = 0.0
var body: some View {
VStack(alignment: .trailing) {
Text(LocalizedStringKey("\(hintKey)\(value)"))
HStack {
Slider(value: $sliderValue, in: 0...5)
.onChange(of: sliderValue, perform: sliderChanged)
}
}
}
private func sliderChanged(to newValue: Double) {
sliderValue = newValue.rounded()
let roundedValue = Int(sliderValue)
if roundedValue == value {
return
}
print("Updating value")
value = roundedValue
}
}
In Version 11.4.1 (11E503a) & Swift 5. I didn't reproduce it.
By using Combine, I could get continuously update from slider changes.
class SliderData: ObservableObject {
#Published var sliderValue: Double = 0
...
}
struct ContentView: View {
#ObservedObject var slider = SliderData()
var body: some View {
VStack {
Slider(value: $slider.sliderValue)
Text(String(slider.sliderValue))
}
}
}
I am not able to reproduce this issue on iOS 13 Beta 2. Which operating system are you targeting?
Using a custom binding, the value is printed for every small change, not only after editing ended.
Slider(value: Binding<Double>(getValue: {0}, setValue: {print($0)}))
Note, that the closure ({ pressed in }) only reports when editing end starts and ends, the value stream is only passed into the binding.
What about like this:
(1) First you need the observable ...
import SwiftUI
import PlaygroundSupport
// make your observable double for the slider value:
class SliderValue: ObservableObject {
#Published var position: Double = 11.0
}
(2) When you make the slider, you have to PASS IN an instance of the observable:
So in HandySlider it is declared as an ObservedObject. (Don't forget, you're not "making" it there. Only declare it as a StateObject where you are "making" it.)
(3) AND you use the "$" for the Slider value as usual in a slider
(It seems the syntax is to use it on the "whole thing" like this "$sliderValue.position" rather than on the value per se, like "sliderValue.$position".)
struct HandySlider: View {
// don't forget to PASS IN a state object when you make a HandySlider
#ObservedObject var sliderValue: SliderValue
var body: some View {
HStack {
Text("0")
Slider(value: $sliderValue.position, in: 0...20)
Text("20")
}
}
}
(4) Actually make the state object somewhere.
(So, you use "StateObject" to do that, not "ObservedObject".)
And then
(5) use it freely where you want to display the value.
struct ContentView: View {
// here we literally make the state object
// (you'd just make it a "global" but not possible in playground)
#StateObject var sliderValue = SliderValue()
var body: some View {
HandySlider(sliderValue: sliderValue)
.frame(width: 400)
Text(String(sliderValue.position))
}
}
PlaygroundPage.current.setLiveView(ContentView())
Test it ...
Here's the whole thing to paste in a playground ...
import SwiftUI
import PlaygroundSupport
class SliderValue: ObservableObject {
#Published var position: Double = 11.0
}
struct HandySlider: View {
#ObservedObject var sliderValue: SliderValue
var body: some View {
HStack {
Text("0")
Slider(value: $sliderValue.position, in: 0...20)
Text("20")
}
}
}
struct ContentView: View {
#StateObject var sliderValue = SliderValue()
var body: some View {
HandySlider(sliderValue: sliderValue)
.frame(width: 400)
Text(String(sliderValue.position))
}
}
PlaygroundPage.current.setLiveView(ContentView())
Summary ...
You'll need an ObservableObject class: those contain Published variables.
Somewhere (obviously one place only) you will literally make that observable object class, and that's StateObject
Finally you can use that observable object class anywhere you want (as many places as needed), and that's ObservedObject
And in a slider ...
In the tricky case of a slider in particular, the desired syntax seems to be
Slider(value: $ooc.pitooc, in: 0...20)
ooc - your observable object class
pitooc - a property in that observable object class
You would not create the observable object class inside the slider, you create it elsewhere and pass it in to the slider. (So indeed in the slider class it is an observed object, not a state object.)
iOS 13.4, Swift 5.x
An answer based on Mohammid excellent solution, only I didn't want to use environmental variables.
class SliderData: ObservableObject {
let didChange = PassthroughSubject<SliderData,Never>()
#Published var sliderValue: Double = 0 {
didSet {
print("sliderValue \(sliderValue)")
didChange.send(self)
}
}
}
#ObservedObject var sliderData:SliderData
Slider(value: $sliderData.sliderValue, in: 0...Double(self.textColors.count))
With a small change to ContentView_Preview and the same in SceneDelegate.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(sliderData: SliderData.init())
}
}
If the value is in a navigation, child view:
Here's the case if the slider is, say, a popup which allows you to adjust a value.
It's actually simpler, nothing needs to be passed in to the slider. Just use an #EnvironmentObject.
Don't forget environment objects must be in the ancestor chain (you can't unfortunately go "sideways").
EnvironmentObject is only for parent-child chains.
Somewhat confusingly, you can't use the simple EnvironmentObject system if the items in question are in the same "environment!" EnvironmentObject should perhaps be named something like "ParentChainObject" or "NavigationViewChainObject".
EnvironmentObject is only used when you are using NavigationView.
import SwiftUI
import PlaygroundSupport
// using ancestor views ...
class SliderValue: ObservableObject {
#Published var position: Double = 11.0
}
struct HandySliderPopUp: View {
#EnvironmentObject var sv: SliderValue
var body: some View {
Slider(value: $sv.position, in: 0...10)
}
}
struct ContentView: View {
#StateObject var sliderValue = SliderValue()
var body: some View {
NavigationView {
VStack{
NavigationLink(destination:
HandySliderPopUp().frame(width: 400)) {
Text("click me")
}
Text(String(sliderValue.position))
}
}
.environmentObject(sliderValue) //HERE
}
}
PlaygroundPage.current.setLiveView(ContentView())
Note that //HERE is where you "set" the environment object.
For the "usual" situation, where it's the "same" view, see other answer.
Late to the party, this is what I did:
struct DoubleSlider: View {
#State var value: Double
let range: ClosedRange<Double>
let step: Double
let onChange: (Double) -> Void
init(initialValue: Double, range: ClosedRange<Double>, step: Double, onChange: #escaping (Double) -> Void) {
self.value = initialValue
self.range = range
self.step = step
self.onChange = onChange
}
var body: some View {
let binding = Binding<Double> {
return value
} set: { newValue in
value = newValue
onChange(newValue)
}
Slider(value: binding, in: range, step: step)
}
}
Usage:
DoubleSlider(initialValue: state.tipRate, range: 0...0.3, step: 0.01) { rate in
viewModel.state.tipRate = rate
}
Just use the onEditingChanged parameter of Slider. The argument is true while the user is moving the slider or still in contact with it. I do my updates when the argument changes from true to false.
struct MyView: View {
#State private var value = 0.5
func update(changing: Bool) -> Void {
// Do whatever
}
var body: some View {
Slider(value: $value, onEditingChanged: {changing in self.update(changing) })
{ pressed in }
}
}

Resources