How to properly use optional properties of structs in Swift - swift

I have the following struct:
extension Models {
struct Book: Identifiable {
let id: UUID
var title: String
var pagesCount: Int
var cover: Optional<Cover>;
struct Cover {
let image: String
init(image: String) {
self.image = image
}
}
init(id: UUID = UUID(), title: String, pagesCount: Int, cover: Optional<Cover> = nil) {
self.id = id
self.title = title
self.pagesCount = pagesCount
self.cover = cover
}
}
}
extension Models.Book {
static let sampleData: Models.Book = Models.Book(title: "Orconomics", pagesCount: 100, cover: Models.Book.Cover(image: "actual_image.png"))
}
and I am creating a SwiftUI view that displays each "model" Book.
I have made the Cover parameter optional, as some books won't have covers, the Models.Book.Cover struct has a non-optional property image.
Here's what I have in the body of my SwiftUI file:
HStack {
if book.cover != nil {
Text(book.cover?.image ?? "test") // <- can I avoid this?
}
}
Isn't the book.cover != nil sufficient for Swift? Why do I need the "default value" (test) and the ? after book.cover?

you could try something like this:
HStack {
if let cover = book.cover {
Text(cover.image)
}
}

What I use to do is handle this in my view model so I guess you should have a default value in your init, so I don't need to handle that in the View, since I like the view to be dumb and not deal with much logic.
I think you should use insteaad guard if you want to add an else option IMO guard is a bit more readable, if not if let as mentioned before should be fine.

View? conforms to View. So just use map.
book.cover?.image.map(Text.init)

Related

Best way to implement expensive derived properties in SwiftUI?

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.

Userdefaults with published enum

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

Why isn't a closure in #State array property triggering state change?

A test view has #State showTitle, title and items where the title value text is controlled by a closure assigned to a CTA show title.
When the showTitle state changes, the value presented in the body Content of test view changes accordingly:
Text({ self.showTitle ? "Yes, showTitle!" : "No, showTitle!" }())
While the case where the closure is a value in the array items does not change. Why isn't the closure triggering the title state?
NestedView(title: $0.title())
I've done tests with both Foobar as Struct and Class.
import SwiftUI
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: () -> String
init (title: #escaping () -> String) {
self.title = title
}
}
struct test: View {
#State var showTitle: Bool = true
#State var title: String
#State var items: [Foobar]
var body: some View {
VStack {
Group {
Text("Case 1")
Text({ self.showTitle ? "Yes, showTitle!" : "No, showTitle!" }())
}
Group {
Text("Case 2")
ForEach (self.items, id: \.id) {
NestedView(title: $0.title())
}
}
Button("show title") {
print("show title cb")
self.showTitle.toggle()
}
}.onAppear {
let data = ["hello", "world", "test"]
for title in data {
self.items.append(Foobar(title: { self.showTitle ? title : "n/a" }))
}
}
}
}
struct NestedView: View {
var title: String
var body: some View {
Text("\(title)")
}
}
What's expected is that "Case 2" to have a similar side-effect we have in "Case 1" that should display "n/a" on showTitle toggle.
Output:
From what I understand, the reason why the initial code does not work is related to the showTitle property that is passed to the Array and holds a copy of the value
You were right to blame the closure capturing the value at the onAppear time. Basically due to this, SwiftUI doesn't know to refresh the list when the showTitle value changes, as there's no Binding involved that SwiftUI can use to know when to re-render the list.
I can provide two alternative solutions, that don't require another class just to hold the bool value. Both solutions involve communicating to SwiftUI that you need the showTitle binding to refresh the titles.
Don't use a closure for title, defer the title computation to the list builder:
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: String
init (title: String) {
self.title = title
}
}
...
ForEach (self.items, id: \.id) {
NestedView(title: self.showTitle ? $0.title : "n/a" )
}
...
.onAppear {
let data = ["hello", "world", "test"]
self.items = data.map { Foobar(title: $0) }
}
Convert the title closure to a (Binding<Bool>) -> String one, inject the $showTitle binding from the view:
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: ((Binding<Bool>) -> String)
init (title: #escaping (Binding<Bool>) -> String) {
self.title = title
}
}
...
ForEach (self.items, id: \.id) {
// here we pass the $showTitle binding, thus SwiftUI knows to re-render
// the view when the binding value is updated
NestedView(title: $0.title(self.$showTitle))
}
...
.onAppear {
let data = ["hello", "world", "test"]
self.items = data.map { Foobar(title: { $0.wrappedValue ? title : "n/a" })) }
}
Personally, I'd go with the first solution, since it better transmit the intent.
From what I understand, the reason why the initial code does not work is related to the showTitle property that is passed to the Array and holds a copy of the value (creates a unique copy of the data).
I did think #State would make it controllable and mutable, and the closure would capture and store the reference (create a shared instance). In other words, to have had a reference, instead of a copied value! Feel free to correct me, if that's not the case, but that's what it looks like based on my analysis.
With that being said, I kept the initial thought process, I still want to pass a closure to the Array and have the state changes propagated, cause side-effects, accordingly to any references to it!
So, I've used the same pattern but instead of relying on a primitive type for showTitle Bool, created a Class that conforms to the protocol ObservableObject: since Classes are reference types.
So, let's have a look and see how this worked out:
import SwiftUI
class MyOption: ObservableObject {
#Published var option: Bool = false
}
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: () -> String
init (title: #escaping () -> String) {
self.title = title
}
}
struct test: View {
#EnvironmentObject var showTitle: MyOption
#State var title: String
#State var items: [Foobar]
var body: some View {
VStack {
Group {
Text("Case 1")
Text(self.showTitle.option ? "Yes, showTitle!" : "No, showTitle!")
}
Group {
Text("Case 2")
ForEach (self.items, id: \.id) {
NestedView(title: $0.title())
}
}
Button("show title") {
print("show title cb")
self.showTitle.option.toggle()
print("self.showTitle.option: ", self.showTitle.option)
}
}.onAppear {
let data = ["hello", "world", "test"]
for title in data {
self.items.append(Foobar(title: { self.showTitle.option ? title : "n/a" }))
}
}
}
}
struct NestedView: View {
var title: String
var body: some View {
Text("\(title)")
}
}
The result as expected:

Is it possible to exclude certain property (Like class type) from being copied during struct copy?

I have the following struct, which contains class.
import Foundation
func generateRichText(body: String?) -> NSMutableAttributedString? {
if body == nil {
return nil
}
// TODO: Some complex logic to decorate body string will be added soon...
let myAttrString = NSMutableAttributedString(string: body!)
return myAttrString
}
struct Note {
var body: String?
// Technique described in https://stackoverflow.com/a/25073176/72437
var bodyAsRichText: NSMutableAttributedString? {
mutating get {
if (cachedBodyAsRichText == nil) {
cachedBodyAsRichText = generateRichText(body: body)
}
return cachedBodyAsRichText
}
}
// TODO: This is a class. I don't want it to be copied over during struct copy.
// If it is copied during struct copy, both struct will be sharing the same
// class instance.
private var cachedBodyAsRichText: NSMutableAttributedString?
}
var note = Note()
note.body = "hello"
print("note.bodyAsRichText = \(Unmanaged.passUnretained(note.bodyAsRichText!).toOpaque())")
var note_copy = note
print("note_copy.bodyAsRichText = \(Unmanaged.passUnretained(note_copy.bodyAsRichText!).toOpaque())")
For the above code, the output will be
note.bodyAsRichText = 0x000055c035cfce70
note_copy.bodyAsRichText = 0x000055c035cfce70
What my desired output is, different struct instance, should be having their very own class instance (cachedBodyAsRichText)
Hence, is there a way, to exclude cachedBodyAsRichText from being copied over, during struct copy?
Your solution is incomplete. Here is a complete and correct solution.
struct Note {
var body: String = "" {
didSet {
cachedBodyAsRichText = nil
}
}
var bodyAsRichText: NSAttributedString {
mutating get {
if (cachedBodyAsRichText == nil) {
cachedBodyAsRichText = generateRichText(body: body)
}
return cachedBodyAsRichText!.copy() as! NSAttributedString
}
}
private var cachedBodyAsRichText: NSAttributedString? = nil
}
You need to clear out the cache every time the body is modified. Once you do that, it won't matter if the object is shared among structs.

How do I use UserDefaults with SwiftUI?

struct ContentView: View {
#State var settingsConfiguration: Settings
struct Settings {
var passwordLength: Double = 20
var moreSpecialCharacters: Bool = false
var specialCharacters: Bool = false
var lowercaseLetters: Bool = true
var uppercaseLetters: Bool = true
var numbers: Bool = true
var space: Bool = false
}
var body: some View {
VStack {
HStack {
Text("Password Length: \(Int(settingsConfiguration.passwordLength))")
Spacer()
Slider(value: $settingsConfiguration.passwordLength, from: 1, through: 512)
}
Toggle(isOn: $settingsConfiguration.moreSpecialCharacters) {
Text("More Special Characters")
}
Toggle(isOn: $settingsConfiguration.specialCharacters) {
Text("Special Characters")
}
Toggle(isOn: $settingsConfiguration.space) {
Text("Spaces")
}
Toggle(isOn: $settingsConfiguration.lowercaseLetters) {
Text("Lowercase Letters")
}
Toggle(isOn: $settingsConfiguration.uppercaseLetters) {
Text("Uppercase Letters")
}
Toggle(isOn: $settingsConfiguration.numbers) {
Text("Numbers")
}
Spacer()
}
.padding(.all)
.frame(width: 500, height: 500)
}
}
So I have all this code here and I want to use UserDefaults to save settings whenever a switch is changed or a slider is slid and to retrieve all this data when the app launches but I have no idea how I would go about using UserDefaults with SwiftUI (Or UserDefaults in general, I've just started looking into it so I could use it for my SwiftUI app but all the examples I see are for UIKit and when I try implementing them in SwiftUI I just run into a ton of errors).
The approach from caram is in general ok but there are so many problems with the code that SmushyTaco did not get it work. Below you will find an "Out of the Box" working solution.
1. UserDefaults propertyWrapper
import Foundation
import Combine
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
2. UserSettings class
final class UserSettings: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#UserDefault("ShowOnStart", defaultValue: true)
var showOnStart: Bool {
willSet {
objectWillChange.send()
}
}
}
3. SwiftUI view
struct ContentView: View {
#ObservedObject var settings = UserSettings()
var body: some View {
VStack {
Toggle(isOn: $settings.showOnStart) {
Text("Show welcome text")
}
if settings.showOnStart{
Text("Welcome")
}
}
}
Starting from Xcode 12.0 (iOS 14.0) you can use #AppStorage property wrapper for such types: Bool, Int, Double, String, URL and Data.
Here is example of usage for storing String value:
struct ContentView: View {
static let userNameKey = "user_name"
#AppStorage(Self.userNameKey) var userName: String = "Unnamed"
var body: some View {
VStack {
Text(userName)
Button("Change automatically ") {
userName = "Ivor"
}
Button("Change manually") {
UserDefaults.standard.setValue("John", forKey: Self.userNameKey)
}
}
}
}
Here you are declaring userName property with default value which isn't going to the UserDefaults itself. When you first mutate it, application will write that value into the UserDefaults and automatically update the view with the new value.
Also there is possibility to set custom UserDefaults provider if needed via store parameter like this:
#AppStorage(Self.userNameKey, store: UserDefaults.shared) var userName: String = "Mike"
and
extension UserDefaults {
static var shared: UserDefaults {
let combined = UserDefaults.standard
combined.addSuite(named: "group.myapp.app")
return combined
}
}
Notice: ff that value will change outside of the Application (let's say manually opening the plist file and changing value), View will not receive that update.
P.S. Also there is new Extension on View which adds func defaultAppStorage(_ store: UserDefaults) -> some View which allows to change the storage used for the View. This can be helpful if there are a lot of #AppStorage properties and setting custom storage to each of them is cumbersome to do.
The code below adapts Mohammad Azam's excellent solution in this video:
import SwiftUI
struct ContentView: View {
#ObservedObject var userDefaultsManager = UserDefaultsManager()
var body: some View {
VStack {
Toggle(isOn: self.$userDefaultsManager.firstToggle) {
Text("First Toggle")
}
Toggle(isOn: self.$userDefaultsManager.secondToggle) {
Text("Second Toggle")
}
}
}
}
class UserDefaultsManager: ObservableObject {
#Published var firstToggle: Bool = UserDefaults.standard.bool(forKey: "firstToggle") {
didSet { UserDefaults.standard.set(self.firstToggle, forKey: "firstToggle") }
}
#Published var secondToggle: Bool = UserDefaults.standard.bool(forKey: "secondToggle") {
didSet { UserDefaults.standard.set(self.secondToggle, forKey: "secondToggle") }
}
}
First, create a property wrapper that will allow us to easily make the link between your Settings class and UserDefaults:
import Foundation
#propertyWrapper
struct UserDefault<Value: Codable> {
let key: String
let defaultValue: Value
var value: Value {
get {
let data = UserDefaults.standard.data(forKey: key)
let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(data, forKey: key)
}
}
}
Then, create a data store that holds your settings:
import Combine
import SwiftUI
final class DataStore: BindableObject {
let didChange = PassthroughSubject<DataStore, Never>()
#UserDefault(key: "Settings", defaultValue: [])
var settings: [Settings] {
didSet {
didChange.send(self)
}
}
}
Now, in your view, access your settings:
import SwiftUI
struct SettingsView : View {
#EnvironmentObject var dataStore: DataStore
var body: some View {
Toggle(isOn: $settings.space) {
Text("\(settings.space)")
}
}
}
If you are persisting a one-off struct such that a property wrapper is overkill, you can encode it as JSON. When decoding, use an empty Data instance for the no-data case.
final class UserData: ObservableObject {
#Published var profile: Profile? = try? JSONDecoder().decode(Profile.self, from: UserDefaults.standard.data(forKey: "profile") ?? Data()) {
didSet { UserDefaults.standard.set(try? JSONEncoder().encode(profile), forKey: "profile") }
}
}
I'm supriced no one wrote the new way, anyway, Apple migrated to this method now and you don't need all the old code, you can read and write to it like this:
#AppStorage("example") var example: Bool = true
that's the equivalent to read/write in the old UserDefaults. You can use it as a regular variable.
Another great solution is to use the unofficial static subscript API of #propertyWrapper instead of the wrappedValue which simplifies a lot the code. Here is the definition:
#propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(wrappedValue: Value, _ key: String) {
self.key = key
self.defaultValue = wrappedValue
}
var wrappedValue: Value {
get { fatalError("Called wrappedValue getter") }
set { fatalError("Called wrappedValue setter") }
}
static subscript(
_enclosingInstance instance: Preferences,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
) -> Value {
get {
let wrapper = instance[keyPath: storageKeyPath]
return instance.userDefaults.value(forKey: wrapper.key) as? Value ?? wrapper.defaultValue
}
set {
instance.objectWillChange.send()
let key = instance[keyPath: storageKeyPath].key
instance.userDefaults.set(newValue, forKey: key)
}
}
}
Then you can define your settings object like this:
final class Settings: ObservableObject {
let userDefaults: UserDefaults
init(defaults: UserDefaults = .standard) {
userDefaults = defaults
}
#UserDefaults("yourKey") var yourSetting: SettingType
...
}
However, be careful with this kind of implementation. Users tend to put all their app settings in one of such object and use it in every view that depends on one setting. This can result in slow down caused by too many unnecessary objectWillChange notifications in many view.
You should definitely separate concerns by breaking down your settings in many small classes.
The #AppStorage is a great native solution but the drawback is that is kind of break the unique source of truth paradigm as you must provide a default value for every property.