I've got a Published variable that changes when a function is called. I print the variable within the class and I can see that it has set the variable but when it doesn't change my View, and when I print the variable it's only printing what I initially set the variable at.
I know there have been a couple similar posts on this, but after going through them I'm still not sure what I'm doing wrong.
Code is as follows: (minus bits I think aren't relevant here)
Class containing function that is called to change the variable
class MusicManager: NSObject, ObservableObject {
#Published var playlistLabel: String = "N/A"
func playPlaylistNow(chosenPlaylist: String?) {
...
playlistLabel = chosenPlaylist!
print(playlistLabel) // I get the revised variable printed here
}
}
View to Update
struct HomeView: View {
...
#ObservedObject var musicManager: MusicManager = MusicManager()
var body: some View {
...
SongLabels(trackLabel: currentTrack!, artistLabel: currentArtist!, playlistLabel: musicManager.playlistLabel)
...
}
...
.onAppear {
self.updateTrackData()
}
}
func updateTrackData() {
print("Playlist: \(musicManager.playlistLabel)") // I get the original "N/A" printed here
}
View that calls the function:
{
#State private var showingAlert = false
let musicManager: MusicManager = MusicManager()
var playlistName: String
var body: some View {
Button(action: {
self.showingAlert = true
self.musicManager.playPlaylistNext(chosenPlaylist: self.playlistName)
}) {
Text("Play Next")
}
.alert(isPresented: $showingAlert) {
...
}
...
}
}
You have more than one instance of MusicManager in your app, so they are not identical.
If you would like to print, you can print both musicManager , to see the difference.
Related
I'm curious, has anyone seen this before, or do they know how to solve it? I have a situation where editing a textfield that's in a NavigationStack always pops the text cursor to the end of the field on every keystroke. I suspect it has something to do with SwiftUI's management of views and state, but I am not spotting anything unusual that I might be doing, other than the index lookup in the navigationDestination part. I don't understand why that would be a problem.
Here's some pretty minimal code demonstrating the problem (just try correcting the well-known Shakespeare quote):
struct CursorResetting: View {
struct Record: Identifiable {
var string = ""
var id = UUID()
}
#State var path = NavigationPath()
#State private var records = [
Record(string: "To be and not to be"),
Record(string: "That begs the question")
]
var body: some View {
NavigationStack(path: $path) {
Form {
List {
ForEach(records) { record in
NavigationLink(value: record.id) {
Text(record.string)
}
}
}
}
.navigationDestination(for: UUID.self) { id in
let index = records.firstIndex { $0.id == id }
if let index {
Form {
TextField("Value", text: $records[index].string)
}
} else {
Text("Invalid ID")
}
}
}
}
}
This is a known thing, and the reason why you can't use a Binding type with navigationDestination.
Binding triggers the View to redraw and it resets everything in the body including the navigationDestination modifier.
You have to use NavigationLink(destination:, label:)
import SwiftUI
struct CursorResettingView: View {
struct Record: Identifiable {
var string = ""
var id = UUID()
}
#State var path = NavigationPath()
#State private var records = [
Record(string: "To be and not to be"),
Record(string: "That begs the question")
]
var body: some View {
NavigationStack(path: $path) {
Form {
List {
ForEach($records) { $record in
NavigationLink {
Form {
TextField("Value", text: $record.string)
}
} label: {
Text(record.string)
}
}
}
}
.navigationDestination(for: UUID.self) { id in
//Use this for `View`s that don't use `Binding` type as argument.
}
}
}
}
struct CursorResettingView_Previews: PreviewProvider {
static var previews: some View {
CursorResettingView()
}
}
Anything that uses an array's index is generally considered unsafe with SwiftUI so "solutions" that depend on it will be inherently fragile.
I am trying to set a default text on a TextView when the view appears, while being able to still keep track of changes to the TextView that I can then pass on to my ViewModel.
Here is a little example that looks like what I am trying to do. This does however not work, it does not update the state as I would have expected. Am I doing something wrong?
struct NoteView: View {
#State var note = ""
var noteFromOutside: String?
var body: some View {
VStack {
TextField("Write a note...", text: $note)
.onSubmit {
//Do something with the note.
}
}
.onAppear {
if let newNote = noteFromOutside {
note = newNote
}
}
}
}
struct ParentView: View {
var note = "Note"
var body: some View {
VStack {
NoteView(noteFromOutside: note)
}
}
}
Found this answer to another post which solved my problem. The key was in the #Binding and init().
https://stackoverflow.com/a/64526620/12764203
Here is my code.
Run the MainView struct, and click on the button which should update the word first to the word hello.
It does not update at all even though the logs show that the data is correctly updated. Therefore is there no way to get the view to update when a value changes inside an enum?
The only way I got it to work was a nasty hack. To try the hack just uncomment the 3 lines of commented code and try it. Is there a better way?
I looked at this similar question, but the same problem is there -> SwiftUI two-way binding to value inside ObservableObject inside enum case
struct MainView: View {
#State var selectedEnum = AnEnum.anOption(AnObservedObject(string: "first"))
// #State var update = false
var body: some View {
switch selectedEnum {
case .anOption(var value):
VStack {
switch selectedEnum {
case .anOption(let val):
Text(val.string)
}
TestView(object: Binding(get: { value }, set: { value = $0 }),
callbackToVerifyChange: callback)
}
// .id(update)
}
}
func callback() {
switch selectedEnum {
case .anOption(let value):
print("Callback function shows --> \(value.string)")
// update.toggle()
}
}
}
class AnObservedObject: ObservableObject {
#Published var string: String
init(string: String) {
self.string = string
}
}
enum AnEnum {
case anOption(AnObservedObject)
}
struct TestView: View {
#Binding var object: AnObservedObject
let callbackToVerifyChange: ()->Void
var body: some View {
Text("Tap here to change the word 'first' to 'hello'")
.border(Color.black).padding()
.onTapGesture {
print("String before tapping --> \(object.string)")
object.string = "hello"
print("String after tapping --> \(object.string)")
callbackToVerifyChange()
}
}
}
You need to declare your enum Equatable.
I am trying a very simple test to just combine a simple Just("JustValue") to a property.
But it did not work.
↓ This is my code
struct ResponseView: View {
private var contentCancellable: AnyCancellable? = nil
#State var content: String = "InitialValue"
var body: some View {
Text(content)
}
init() {
contentCancellable = Just("JustValue").assign(to: \.content, on: self)
}
}
Is there anyone know why the Text shows "InitialValue" instead "JustValue"
This is specific of state property wrapper initialization pass... the external state storage is created later so only one initialisation is applied.
If you want to update it, do it later, when state be already created and linked to view, like
struct ResponseView: View {
#State var content: String = "InitialValue"
var body: some View {
Text(content)
.onAppear {
_ = Just("JustValue").assign(to: \.content, on: self)
}
}
}
the gives UI which you expected.
I have a timer view which I want to reuse and I want to start the timer by my binding variable running like this:
Unfortunately I am (too?) tired this morning to find a solution how to do that. :(
Maybe my solution is bad and there is a much easier way!?
struct TimerView: View {
#State var hours : Int = 0
#State var minutes : Int = 0
#State var seconds : Int = 0
#State var hundred : Int = 0
#Binding var running : Bool
let timer = Timer.publish (every: 0.01, on: .main, in: .common)
#State var cancellable : Cancellable?
func start() {
self.cancellable = timer.connect()
}
func stop() {
cancellable!.cancel()
}
You can do the following, create this extension
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
}
And the the parent view, not TimerView you can have something like this
//[...]
TimerView(running: $running.didSet{ value in /* your code or function here*/ })
//[...]
Check my answer here
UPDATE 1
Based on the comment what I understood is that the you want to start/stop the timer from outside the TimerView.
I found a couple ways to do that
First one declare the TimerView as a variable in the parent view
struct ContentView: View {
#State var running: Bool = false
#State var timerView: TimerView? = nil
init() {
timerView = TimerView(running: $running)
}
// rest...
}
But depending on your init it can be a pain so I created a small extension that allows you to reference the view like so
extension View {
func `referenced`<T>(by reference: Binding<T>) -> (Self?){
DispatchQueue.main.async { reference.wrappedValue = self as! T }
return self
}
}
The usage of this extension is rather simple
struct ContentView: View {
#State var running: Bool = false
#State var timerView: TimerView? = nil
var body: some View {
TimerView(running: $running).referenced(by: $timerView)
}
}
Then you can control it using for instance a button Button(action: self.timerView.start()) { Text("Start Timer") }
If tested this code against your TimerView but seems there's something wrong in the stop() code. I commented the line cancellable!.cancel() and added a print instead and everything is working as expected.
Here's the test code that I used in Xcode Playground
I hope this is the answer you're looking for.