I am using a Picker to select a Currency, which is an Identifiable Class based on a Core Data Entity. I thereby would want that there is no initial selection in the picker. However, somehow that is not working, and I can't select anything in the picker. My code looks like this (explorerViewModel.currencies is a Set of Currency):
#State private var selectedCurrency: Currency?
var body: some View {
Form {
Picker("Currency", selection: $selectedCurrency) {
ForEach(explorerViewModel.currencies) { currency in
Text(currency.name)
}
}
}
Any help what is wrong here?
In case required, that is the code for the explorerViewModel:
class ExplorerViewModel: ObservableObject {
private let moc: NSManagedObjectContext
#Published var currencies: [Currency]
init() {
self.moc = PersistenceController.shared.container.viewContext
currencies = (try? moc.fetch(NSFetchRequest<Currency>(entityName : "Currency"))) ?? []
}
}
I actuallt found the issue: I had to append a tag to make the currency optional.
Text(currency.name).tag(currency as Currency?)
worked.
Related
I don't know how to read a property that is in a struct from a class that is an Observable Object.
Context:
I'm trying to build an app which consists of 2 views:
a custom calendar;
a popup with a header 'Daily Joke', date formatted as 'MM-dd-yyyy' and a joke text that is fetched from Firebase using id. When the user clicks on a date in the calendar, the popup appears and shows the joke for a selected date.
The problem is that the 'currentDate' property (holds the value of the selected date) that I reference in the ObservableObject of the 'getJoke' class won't update when the user selects a different date. It always fetches the joke on today's date and not on the one the user has selected.
Here is the code of:
the custom calendar (selected date is held in the property 'currentDate')
import SwiftUI
import grpc
struct CustomDatePicker: View {
#State var currentDate: Date
#State var dailyJokePopUp = false
//some code here
// When the user selects the date, currentDate property changes to the selected date
.onTapGesture {
currentDate = value.date
}
// Getting selected day for displaying in dailyJokePopUp
func getCurrentDay()->String{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let date = dateFormatter.string(from: currentDate)
return date
}
the class which is an #ObservableObject (I use it to add a listener to the Firebase to fetch the joke text by its id. Here I need to read 'currentDate' which is originally declared and changed in CustomDatePicker. I need to do it to check if 'currentDate' matches the id in Firebase (that way the joke text is fetched from Firebase on the selected date)).
class getJoke : ObservableObject {
#Published var data = [JokeX]()
#Published var noData = false
#Published var currentDate = Date()
//some code here including adding SnapShotListener
let callCDP = CustomDatePicker(currentDate: currentDate).getCurrentDay()
if id == callCDP {
self.data.append(joke_text_imported)}
}
}
}
}
the popup (I call the result of the #ObservableObject to get the display the text fetched from Firebase)
import SwiftUI
struct dailyJokePopUp: View {
#Binding var show: Bool
#ObservedObject var Jokes = getJoke()
var currentDate: Date = Date()
//some code here
ForEach(self.Jokes.data){i in
Text(i.joke_text)
}
//some code here
}
I can suspect something is wrong with how I declare properties. I've tried various wrappers (#Binding, #StateObject), but I got confused and it didn't work. Hope someone can be kind enough to help me solve the problem.
ViewModel
class getJoke: ObservableObject {
#Published var currentDate = Date()
}
View that can change passing data
struct CustomDatePicker: View {
#Binding var currentDate: Date
var body: some View{
VStack {
DatePicker(selection: $currentDate, displayedComponents: .date){
Text("Select your date")
}
.datePickerStyle(.compact)
}
}
}
And put everything together
struct ContentView: View {
#StateObject var vm = getJoke()
var body: some View {
VStack(spacing: 40) {
CustomDatePicker(currentDate: $vm.currentDate)
Button {
print(vm.currentDate)
} label: {
Text("Show selected date")
}
}
}
}
I have a somewhat complicated situation. I'm implementing a simple scrolling chat view in SwiftUI. There's a ChatView with a list of ChatMessageCells. Each Message has a User, and that has a ProfileImage which is a struct made up of a couple of fields (bucket and key, used to construct the URL to the image on the server). This can periodically update on the User class, and can also be nil.
Update: This is not a particularly expensive operation, but let’s say for the sake of argument that it is expensive, and I only want to recompute it once. There basically two ways: recompute when (one of the) source properties changes, or recompute when needed and store the result. I’d like to know the best way to do both.
The view needs to construct the URL, because it needs to specify the desired image size for the image. In this way, the URL is a derived property of the message.user.profileImage property.
I tried using .onChange(of: self.message.user.profileImage) in my ChatMessageCell view hierarchy to then compute the URL and set self.profileImageURL, but you can’t set simple properties. So I adorned self.profileImageURL with #State, which allowed the code to compile, and I assign it in init(). But if it’s #State, that assignment doesn't seem to have any effect.
So, I'm pretty unsure how to do this.
ChatView and ChatMessageCell look like this:
struct ChatView : View
{
#ObservedObject public var stream : ChatStream
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(self.stream.messages) { inMsg in
ChatMessageCell(message: inMsg)
}
}
}
}
}
struct
ChatMessageCell: View
{
public let message : ChatStream.Message
#State var profileImageURL : URL?
init(message inMessage: ChatStream.Message)
{
self.message = inMessage
let image = inMessage.user.profileImage
let url = image?.sharpImageURL(forSize: kProfileImageSize)
self.profileImageURL = url // <-- doesn't assign if #State
}
var body: some View
{
VStack(alignment: .leading)
{
Text("Key: \(self.message.user.profileImage?.key ?? "nil")")
Text("URL: \(self.profileImageURL?.absoluteString ?? "nil")")
// This is just for debugging. Really there's a `KFImage` here that’s
// supposed to async load the image.
}
.onChange(of: self.message.user.profileImage)
{ inVal in
let url = inVal?.sharpImageURL(forSize: kProfileImageSize)
self.profileImageURL = url // <-- can't modify if not #State
}
}
}
Other classes:
class ChatStream
{
public struct Message
{
var id : Int
var date : Date
var message : String
var user : User
init(fromIncoming inMsg: IncomingMessage, user inUser: User)
{
self.id = inMsg.id
self.date = inMsg.date
self.message = inMsg.message
self.user = inUser
}
}
public class User
{
typealias ID = String
let id : ID
var username : String
var onlineAt : Date?
#Published var profileImage : ProfileImage?
init(fromIncoming inUser: IncomingUser)
{
self.id = inUser.id
self.username = inUser.username
self.onlineAt = inUser.onlineAt
self.profileImage = inUser.profileImage
}
func update(fromIncoming inUser: IncomingUser)
{
self.username = inUser.username
self.onlineAt = inUser.onlineAt
self.profileImage = inUser.profileImage
}
}
#Published var messages = OrderedSet<Message>()
#Published var users = [User.ID : User]()
}
extension ChatStream : ObservableObject {}
extension ChatStream.ProfileImage : Equatable {}
It would be more straightforward to have message be an observable object, which triggers changes when the user property updates, and have the URL be a computed property of message. You haven't shown the implementation of Message or User so it's hard to be specific. If Message is a struct then you could just have a reference to the user as an observable object.
try to save user setting, but UserDefaults is not working, Xcode 12.3, swiftui 2.0, when I am reload my app, my setting not updating for new value)
class PrayerTimeViewModel: ObservableObject {
#Published var lm = LocationManager()
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults.standard.set(method.params, forKey: "method")
self.getPrayerTime()
}
}
func getPrayerTime() {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: lm.location?.latitude ?? 0.0, longitude: lm.location?.longitude ?? 0.0)
var par = method.params
par.madhab = mashab
self.times = PrayerTimes(coordinates: coordinates, date: date, calculationParameters: par)
}
and view.. update with AppStorage
struct MethodView: View {
#ObservedObject var model: PrayerTimeViewModel
#Environment(\.presentationMode) var presentationMode
#AppStorage("method", store: UserDefaults(suiteName: "method")) var method: CalculationMethod = .dubai
var body: some View {
List(CalculationMethod.allCases, id: \.self) { item in
Button(action: {
self.model.objectWillChange.send()
self.presentationMode.wrappedValue.dismiss()
self.model.method = item
method = item
}) {
HStack {
Text("\(item.rawValue)")
if model.method == item {
Image(systemName: "checkmark")
.foregroundColor(.black)
}
}
}
}
}
}
You have two issues.
First, as I mentioned in my comment above that you are using two different suites for UserDefaults. This means that you are storing and retrieving from two different locations. Either use UserDefaults.standard or use the one with your chosen suite UserDefaults(suitName: "method") - you don't have to use a suite unless you plan on sharing your defaults with other extensions then it would be prudent to do so.
Secondly you are storing the wrong item in UserDefaults. You are storing a computed property params rather than the actual enum value. When you try to retrieve the value it fails as it is not getting what it expects and uses the default value that you have set.
Here is a simple example that shows what you could do. There is a simple enum that has a raw value (String) and conforms to Codable, it also has a computed property. This matches your enum.
I have added an initialiser to my ObservableObject. This serves the purpose to populate my published Place from UserDefaults when the Race object is constructed.
Then in my ContentView I update the place depending on a button press. This updates the UI and it updates the value in UserDefaults.
This should be enough for you to understand how it works.
enum Place: String, Codable {
case first
case second
case third
case notPlaced
var someComputedProperty: String {
"Value stored: \(self.rawValue)"
}
}
class Race: ObservableObject {
#Published var place: Place = .notPlaced {
didSet {
// Store the rawValue of the enum into UserDefaults
// We can store the actual enum but that requires more code
UserDefaults.standard.setValue(place.rawValue, forKey: "method")
// Using a custom suite
// UserDefaults(suiteName: "method").setValue(place.rawValue, forKey: "method")
}
}
init() {
// Load the value from UserDefaults if it exists
if let rawValue = UserDefaults.standard.string(forKey: "method") {
// We need to nil-coalesce here as this is a failable initializer
self.place = Place(rawValue: rawValue) ?? .notPlaced
}
// Using a custom suite
// if let rawValue = UserDefaults(suiteName: "method")?.string(forKey: "method") {
// self.place = Place(rawValue: rawValue) ?? .notPlaced
// }
}
}
struct ContentView: View {
#StateObject var race: Race = Race()
var body: some View {
VStack(spacing: 20) {
Text(race.place.someComputedProperty)
.padding(.bottom, 20)
Button("Set First") {
race.place = .first
}
Button("Set Second") {
race.place = .second
}
Button("Set Third") {
race.place = .third
}
}
}
}
Addendum:
Because the enum conforms to Codable it would be possible to use AppStorage to read and write the property. However, that won't update the value in your ObservableObject so they could easily get out of sync. It is best to have one place where you control a value. In this case your ObservableObject should be the source of truth, and all updates (reading and writing to UserDefaults) should take place through there.
You write in one UserDefaults domain but read from the different. Assuming your intention is to use suite only UserDefaults, you should change one in model, like
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults(suiteName: "method").set(method.params, forKey: "method")
self.getPrayerTime()
}
}
or if you want to use standard then just use AppStorage with default constructor, like
// use UserDefaults.standard by default
#AppStorage("method") var method: CalculationMethod = .dubai
I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}
I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}