didset not working anymore - trying to find workaround - swiftui

i found this answer to get the didset back again:
extension Binding {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print($0) }, in: 0...10)
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
now i tried the same with Published, but got error and don't know how to fix.
Error is: Type of expression is ambiguous without more context
#available(iOS 13.0, *)
extension Published {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print($0) }, in: 0...10)
func didSet(execute: #escaping (Value) ->Void) -> Published<Any> {
return Published ( // error here : Type of expression is ambiguous without more contex`enter code here`
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}

It works, but really for "did set only"... ie.. after direct assignment.
Please consider the following example. Tested with Xcode 11.4 / iOS 13.4
struct FooView: View {
#ObservedObject var test = Foo()
var body: some View {
VStack {
Button("Test Assign") { self.test.foo = 10 } // << didSet works !!
Button("Test Modify") { self.test.foo += 1 } // xx nope !!
Divider()
Text("Current: \(test.foo)")
}
}
}
class Foo: ObservableObject {
#Published var foo: Int = 1 {
didSet {
print("foo ==> \(foo)")
}
}
}

Related

How to display an Error Alert in SwiftUI?

Setup:
I have a SwiftUI View that can present alerts. The alerts are provided by an AlertManager singleton by setting title and/or message of its published property #Published var nextAlertMessage = ErrorMessage(title: nil, message: nil). The View has a property #State private var presentingAlert = false.
This works when the following modifiers are applied to the View:
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
Problem:
Since alerts are also to be presented in other views, I wrote the following custom view modifier:
struct ShowAlert: ViewModifier {
#Binding var presentingAlert: Bool
let alertManager = AlertManager.shared
func body(content: Content) -> some View {
return content
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
and applied it to the View as:
.modifier(ShowAlert(presentingAlert: $presentingAlert))
However, no alerts are now shown.
Question:
What is wrong with my code and how to do it right?
Edit (as requested by Ashley Mills):
Here is a minimal reproducible example.
Please note:
In ContentView, the custom modifier ShowAlert has been out commented. This version of the code shows the alert.
If instead the modifiers .onAppear, .onChange and .alert are out commented, and the custom modifier is enabled, the alert is not shown.
// TestViewModifierApp
import SwiftUI
#main
struct TestViewModifierApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var alertManager = AlertManager.shared
#State private var presentingAlert = false
var body: some View {
let alertManager = AlertManager.shared
let _ = alertManager.showNextAlertMessage(title: "Title", message: "Message")
Text("Hello, world!")
// .modifier(ShowAlert(presentingAlert: $presentingAlert))
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
// AlertManager
import SwiftUI
struct ErrorMessage: Equatable {
let title: String?
let message: String?
var joinedTitle: String {
(title ?? "") + "\n\n" + (message ?? "")
}
static func == (lhs: ErrorMessage, rhs: ErrorMessage) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message
}
}
final class AlertManager: NSObject, ObservableObject {
static let shared = AlertManager() // Instantiate the singleton
#Published var nextAlertMessage = ErrorMessage(title: nil, message: nil)
func showNextAlertMessage(title: String?, message: String?) {
DispatchQueue.main.async {
// Publishing is only allowed from the main thread
self.nextAlertMessage = ErrorMessage(title: title, message: message)
}
}
func alertConfirmed() {
showNextAlertMessage(title: nil, message: nil)
}
}
// ShowAlert
import SwiftUI
struct ShowAlert: ViewModifier {
#Binding var presentingAlert: Bool
let alertManager = AlertManager.shared
func body(content: Content) -> some View {
return content
.onAppear() {
if !(alertManager.nextAlertMessage.title == nil && alertManager.nextAlertMessage.message == nil) {
presentingAlert = true
}
}
.onChange(of: alertManager.nextAlertMessage) { alertMessage in
presentingAlert = alertMessage.title != nil || alertMessage.message != nil
}
.alert(alertManager.nextAlertMessage.joinedTitle, isPresented: $presentingAlert) {
Button("OK", role: .cancel) {
alertManager.alertConfirmed()
}
}
}
}
You're over complicating this, the way to present an error alert is as follows:
Define an object that conforms to LocalizedError. The simplest way to do it is an enum, with a case for each error your app can encounter. You have to implement var errorDescription: String?, this is displayed as the alert title. If you want to display an alert message, then add a method to your enum to return this.
enum MyError: LocalizedError {
case basic
var errorDescription: String? {
switch self {
case .basic:
return "Title"
}
}
var errorMessage: String? {
switch self {
case .basic:
return "Message"
}
}
}
You need a #State variable to hold the error and one that's set when the alert should be presented. You can do it like this:
#State private var error: MyError?
#State private var isShowingError: Bool
but then you have two sources of truth, and you have to remember to set both each time. Alternatively, you can use a computed property for the Bool:
var isShowingError: Binding<Bool> {
Binding {
error != nil
} set: { _ in
error = nil
}
}
To display the alert, use the following modifier:
.alert(isPresented: isShowingError, error: error) { error in
// If you want buttons other than OK, add here
} message: { error in
if let message = error.errorMessage {
Text(message)
}
}
4. Extra Credit
As you did above, we can move a bunch of this stuff into a ViewModifier, so we end up with:
enum MyError: LocalizedError {
case basic
var errorDescription: String? {
switch self {
case .basic:
return "Title"
}
}
var errorMessage: String? {
switch self {
case .basic:
return "Message"
}
}
}
struct ErrorAlert: ViewModifier {
#Binding var error: MyError?
var isShowingError: Binding<Bool> {
Binding {
error != nil
} set: { _ in
error = nil
}
}
func body(content: Content) -> some View {
content
.alert(isPresented: isShowingError, error: error) { _ in
} message: { error in
if let message = error.errorMessage {
Text(message)
}
}
}
}
extension View {
func errorAlert(_ error: Binding<MyError?>) -> some View {
self.modifier(ErrorAlert(error: error))
}
}
Now to display an error, all we need is:
struct ContentView: View {
#State private var error: MyError? = .basic
var body: some View {
Text("Hello, world!")
.errorAlert($error)
}
}

SwiftUI: Custom binding that get value from #StateObject property is set back to old value after StateObject property change

I'm trying to implement a passcode view in iOS. I was following the guide here.
I'm trying to improve it a bit so it allows me to create a passcode by enter same passcode twice. I added a "state" property to the #StateObject and want to clear entered passcode after user input the passcode first time.
Here is my current code:
LockScreenModel.swift
====================
import Foundation
class LockScreenModel: ObservableObject {
#Published var pin: String = ""
#Published var showPin = false
#Published var isDisabled = false
#Published var state = LockScreenState.normal
}
enum LockScreenState: String, CaseIterable {
case new
case verify
case normal
case remove
}
====================
LockScreen.swift
====================
import SwiftUI
struct LockScreen: View {
#StateObject var lockScreenModel = LockScreenModel()
let initialState: LockScreenState
var handler: (String, LockScreenState, (Bool) -> Void) -> Void
var body: some View {
VStack(spacing: 40) {
Text(NSLocalizedString("lock.label.\(lockScreenModel.state.rawValue)", comment: "")).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
}
.onAppear(perform: {lockScreenModel.state = initialState})
.onDisappear(perform: {
lockScreenModel.pin = ""
lockScreenModel.showPin = false
lockScreenModel.isDisabled = false
lockScreenModel.state = .normal
})
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<6) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 30, weight: .thin, design: .default))
Spacer()
}
}
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { lockScreenModel.pin }, set: { newValue in
if newValue.last?.isWholeNumber == true {
lockScreenModel.pin = newValue
}
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
.disabled(lockScreenModel.isDisabled)
}
private var showPinStack: some View {
HStack {
Spacer()
if !lockScreenModel.pin.isEmpty {
showPinButton
}
}
.frame(height: 20)
.padding([.trailing])
}
private var showPinButton: some View {
Button(action: {
lockScreenModel.showPin.toggle()
}, label: {
lockScreenModel.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
guard !lockScreenModel.pin.isEmpty else {
lockScreenModel.showPin = false
return
}
if lockScreenModel.pin.count == 6 {
lockScreenModel.isDisabled = true
handler(lockScreenModel.pin, lockScreenModel.state) { isSuccess in
if isSuccess && lockScreenModel.state == .new {
lockScreenModel.state = .verify
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
} else if !isSuccess {
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
print("this has to called after showing toast why is the failure")
}
}
}
// this code is never reached under normal circumstances. If the user pastes a text with count higher than the
// max digits, we remove the additional characters and make a recursive call.
if lockScreenModel.pin.count > 6 {
lockScreenModel.pin = String(lockScreenModel.pin.prefix(6))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= lockScreenModel.pin.count {
return "circle"
}
if lockScreenModel.showPin {
return lockScreenModel.pin.digits[index].numberString + ".circle"
}
return "circle.fill"
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
====================
The problem is the line lockScreenModel.state = .verify. If I include this line, the passcode TextField won't get cleared, but if I remove this line, the passcode TextField is cleared.
If I add a breakpoint in set method of boundPin, I can see after set pin to empty and state to verify, the set method of boundPin is called with newValue of the old pin which I have no idea why. If I only set pin to empty but don't set state to verify, that set method of boundPin won't get called which confuse me even more. I can't figure out which caused this strange behavior.

How to edit a list of subclass objects

I have a list of items of different classes derived from the same class.
The goal: editing any object using a different view
The model:
class Paper: Hashable, Equatable {
var name: String
var length: Int
init() {
name = ""
length = 0
}
init(name: String, length: Int) {
self.name = name
self.length = length
}
static func == (lhs: Paper, rhs: Paper) -> Bool {
return lhs.length == rhs.length
}
func hash(into hasher: inout Hasher) {
hasher.combine(length)
}
}
class ScientificPaper: Paper {
var biology: Bool
override init(name: String, length: Int) {
biology = false
super.init(name: name, length: length)
}
}
class TechnicalPaper: Paper {
var electronics: Bool
override init(name: String, length: Int) {
electronics = false
super.init(name: name, length: length)
}
}
The main view containing the list.
struct TestView: View {
#Binding var papers: [Paper]
#State private var edit = false
#State private var selectedPaper = Paper()
var body: some View {
let scientificBinding = Binding<ScientificPaper>(
get: {selectedPaper as! ScientificPaper},
set: { selectedPaper = $0 }
)
VStack {
List {
ForEach(papers, id: \.self) { paper in
HStack {
Text(paper.name)
Text("\(paper.length)")
Spacer()
Button("Edit") {
selectedPaper = paper
edit = true
}
}
}
}
}
.sheet(isPresented: $edit) {
VStack {
if selectedPaper is ScientificPaper {
ScientificForm(paper: scientificBinding)
}
if selectedPaper is TechnicalPaper {
TechnicalForm(paper: technicalBinding)
}
}
}
}
}
The custom view for each class.
struct ScientificForm: View {
#Binding var paper: ScientificPaper
var body: some View {
Form {
Text("Scientific")
TextField("Name: ", text: $paper.name)
TextField("Length: ", value: $paper.length, formatter: NumberFormatter())
TextField("Biology: ", value: $paper.biology, formatter: NumberFormatter())
}
}
}
struct TechnicalForm: View {
#Binding var paper: TechnicalPaper
var body: some View {
Form {
Text("Technical")
TextField("Name: ", text: $paper.name)
TextField("Length: ", value: $paper.length, formatter: NumberFormatter())
TextField("Electronics: ", value: $paper.electronics, formatter: NumberFormatter())
}
}
}
Problem is that at run time I get the following:
Could not cast value of type 'Paper' to 'ScientificPaper'.
maybe because the selectedPaper is already initialized as Paper.
What is the right strategy to edit list items belonging to different classes?
The error is due to creating binding in body, which calculates on every refresh, so binding is invalid.
The solution is to make binding as computable property, so it is requested only after validation in correct flow.
Tested with Xcode 12.1 / iOS 14.1 (demo is for scientificBinding only for simplicity)
struct TestView: View {
#Binding var papers: [Paper]
#State private var edit = false
#State private var selectedPaper = Paper()
var scientificBinding: Binding<ScientificPaper> { // << here !!
return Binding<ScientificPaper>(
get: {selectedPaper as! ScientificPaper},
set: { selectedPaper = $0 }
)
}
var body: some View {
VStack {
List {
ForEach(papers, id: \.self) { paper in
HStack {
Text(paper.name)
Text("\(paper.length)")
Spacer()
Button("Edit") {
selectedPaper = paper
edit = true
}
}
}
}
}
.sheet(isPresented: $edit) {
VStack {
if selectedPaper is ScientificPaper {
ScientificForm(paper: scientificBinding)
}
// if selectedPaper is TechnicalPaper {
// TechnicalForm(paper: technicalBinding)
// }
}
}
}
}

SwiftUI: Real device shows strange behavior with asynchronous flow, while Simulator runs perfectly

*** EDIT 23.20.20 ***
Due to the strange behavior discovered after my original post, I need to completely rephrase my question. I meanwhile re-wrote large parts of my code as well.
The issue:
I run an asynchronous HTTP GET search query, which returns me an Array searchResults, which I store in an ObservedObject FoodDatabaseResults.
struct FoodItemEditor: View {
//...
#ObservedObject var foodDatabaseResults = FoodDatabaseResults()
#State private var activeSheet: FoodItemEditorSheets.State?
//...
var body: some View {
NavigationView {
VStack {
Form {
Section {
HStack {
// Name
TextField(titleKey: "Name", text: $draftFoodItem.name)
// Search and Scan buttons
Button(action: {
if draftFoodItem.name.isEmpty {
self.errorMessage = NSLocalizedString("Search term must not be empty", comment: "")
self.showingAlert = true
} else {
performSearch()
}
}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}.buttonStyle(BorderlessButtonStyle())
//...
}
//...
}
//...
}
}
//...
}
.sheet(item: $activeSheet) {
sheetContent($0)
}
}
private func performSearch() {
UserSettings.shared.foodDatabase.search(for: draftFoodItem.name) { result in
switch result {
case .success(let networkSearchResults):
guard let searchResults = networkSearchResults else {
return
}
DispatchQueue.main.async {
self.foodDatabaseResults.searchResults = searchResults
self.activeSheet = .search
}
case .failure(let error):
debugPrint(error)
}
}
}
#ViewBuilder
private func sheetContent(_ state: FoodItemEditorSheets.State) -> some View {
switch state {
case .search:
FoodSearch(foodDatabaseResults: foodDatabaseResults, draftFoodItem: self.draftFoodItem) // <-- I set a breakpoint here
//...
}
}
}
class FoodDatabaseResults: ObservableObject {
#Published var selectedEntry: FoodDatabaseEntry?
#Published var searchResults: [FoodDatabaseEntry]?
}
I get valid search results in my performSearch function. The DispatchQueue.main.async closure makes sure to perform the update of my #Published var searchResults in the main thread.
I then open a sheet, displaying these search results:
struct FoodSearch: View {
#ObservedObject var foodDatabaseResults: FoodDatabaseResults
#Environment(\.presentationMode) var presentation
//...
var body: some View {
NavigationView {
List {
if foodDatabaseResults.searchResults == nil {
Text("No search results (yet)")
} else {
ForEach(foodDatabaseResults.searchResults!) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
}
.navigationBarTitle("Food Database Search")
.navigationBarItems(leading: Button(action: {
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}) {
Text("Cancel")
}, trailing: Button(action: {
if selectedResult == nil {
//...
} else {
//... Do something with the result
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}
}) {
Text("Select")
})
}
}
}
When I run this on the Simulator, everything works as it should, see https://wolke.rueth.info/index.php/s/KbqETcDtSe4278d
When I run it on a real device with the same iOS version (14.0.1), the FoodSearch view first correctly displays the search result, but is then immediately called a second time with empty (nil) search results. You need to look very closely at the screen cast here and you'll see it displaying the search results for a very short moment before they disappear: https://wolke.rueth.info/index.php/s/9n2DZ88qSB9RWo4
When setting a breakpoint in the marked line in my sheetContent function, the FoodSearch sheet is indeed called twice on the real device, while it's only called once in the Simulator.
I have no idea what is going on here. Hope someone can help. Thanks!
*** ORIGINAL POST ***
I run an HTTP request, which updates a #Published variable searchResults in a DispatchQueue.main.async closure:
class OpenFoodFacts: ObservableObject {
#Published var searchResults = [OpenFoodFactsProduct]()
// ...
func search(for term: String) {
let urlString = "https://\(countrycode)-\(languagecode).openfoodfacts.org/cgi/search.pl?action=process&search_terms=\(term)&sort_by=unique_scans_n&json=true"
let request = prepareRequest(urlString)
let session = URLSession.shared
session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
guard error == nil else {
debugPrint(error!.localizedDescription)
return
}
if let data = data {
do {
let openFoodFactsSearchResult = try JSONDecoder().decode(OpenFoodFactsSearchResult.self, from: data)
guard let products = openFoodFactsSearchResult.products else {
throw FoodDatabaseError.noSearchResults
}
DispatchQueue.main.async {
self.searchResults = products
self.objectWillChange.send()
}
} catch {
debugPrint(error.localizedDescription)
}
}
}).resume()
}
struct OpenFoodFactsSearchResult: Decodable {
var products: [OpenFoodFactsProduct]?
enum CodingKeys: String, CodingKey {
case products
}
}
struct OpenFoodFactsProduct: Decodable, Hashable, Identifiable {
var id = UUID()
// ...
enum CodingKeys: String, CodingKey, CaseIterable {
// ...
}
// ...
}
I call the search function from my view:
struct FoodSearch: View {
#ObservedObject var foodDatabase: OpenFoodFacts
// ...
var body: some View {
NavigationView {
List {
ForEach(foodDatabase.searchResults) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
// ...
}
.onAppear(perform: search)
}
private func search() {
foodDatabase.search(for: draftFoodItem.name)
}
}
My ForEach list will never update, although I have a valid searchResult set in my OpenFoodFacts observable object and also sent an objectWillChange signal. Any idea what I'm missing?
Funny enough: On the simulator it works as expected:
https://wolke.rueth.info/index.php/s/oy4Xf6C5cgrEZdK
On a real device not:
https://wolke.rueth.info/index.php/s/TQz8HnFyjLKtN74

Confirm from model in SwiftUI

let us imagine that I have something like the following core/model:
class Core: ObservableObject {
...
func action(confirm: () -> Bool) {
if state == .needsConfirmation, !confirm() {
return
}
changeState()
}
...
}
and then I use this core object in a SwiftUI view.
struct ListView: View {
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
core.action {
// present an alert to the user and return if the user confirms or not
}
}
}
}
}
So boiling it down, I wonder how to work with handlers there need an input from the user, and I cant wrap my head around it.
It looks like you reversed interactivity concept, instead you need something like below (scratchy)
struct ListView: View {
#State private var confirmAlert = false
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
if core.needsConfirmation {
self.confirmAlert = true
} else {
self.core.action() // << direct action
}
}
}
.alert(isPresented: $confirmAlert) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .default(Text("Confirm")) {
self.core.needsConfirmation = false
self.core.action() // <<< confirmed action
},
secondaryButton: .cancel())
}
}
}
class Core: ObservableObject {
var needsConfirmation = true
...
func action() {
// just act
}
...
}
Alternate: with hidden condition checking in Core
struct ListView: View {
#ObservedObject core: Core
...
var body: some View {
List(objects) {
Text($0)
.onTapGesture {
self.core.action() // << direct action
}
}
.alert(isPresented: $core.needsConfirmation) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .default(Text("Confirm")) {
self.core.action(state: .confirmed) // <<< confirmed action
},
secondaryButton: .cancel())
}
}
}
class Core: ObservableObject {
#Published var needsConfirmation = false
...
func action(state: State = .check) {
if state == .check && self.state != .confirmed {
self.needsConfirmation = true
return;
}
self.state = state
// just act
}
...
}