SwiftUI: Binding to #AppStorage - swift

In the following example how can I change the value of activeSheet based on how SwiftUI updates aArrived and bArrived?
struct ContentView: View {
#AppStorage("didAArrive") var aArrived: Bool = false
#AppStorage("didBArrive") var bArrived: Bool = false
enum ActiveSheet: Identifiable {
case aArrived, bArrived
var id: Int {
hashValue
}
}
#State private var activeSheet: ActiveSheet?
var body: some View {
Text("Hello")
.sheet(
item: $activeSheet,
content: { item in
switch item {
case .aArrived:
Text("A arrived")
case .bArrived:
Text("B arrived")
}
}
)
}
}

You can create a custom binding for the sheet which gets its value based on aArrived and bArrived. The binding value will be initialised based on aArrived or bArrived and get updated every time that either one changes.
struct ContentView: View {
#AppStorage("didAArrive") var aArrived: Bool = false
#AppStorage("didBArrive") var bArrived: Bool = false
enum ActiveSheet: Identifiable {
case aArrived, bArrived
var id: Int {
hashValue
}
}
var body: some View {
let sheetBinding = Binding<ActiveSheet?>(
get: {
if aArrived && bArrived {
return ActiveSheet.aArrived
} else if aArrived {
return ActiveSheet.aArrived
} else if bArrived {
return ActiveSheet.bArrived
} else {
return nil
}
},
set: { _ in }
)
VStack(spacing: 20) {
Toggle("A arrived", isOn: $aArrived)
Toggle("B arrived", isOn: $bArrived)
}
.sheet(
item: sheetBinding,
content: { item in
switch item {
case .aArrived:
Text("A arrived")
case .bArrived:
Text("B arrived")
}
}
)
}
}

Related

SwiftUI Picker not changing selection value

The following picker isn't updating $selection. Regardless of what the Picker shows while running the app, selection.rawValue always returns 0.
What is preventing the Picker from updating the State variable selection?
import SwiftUI
struct OrderPicker: View {
let initialIndex: Int
#State private var selection: Index
enum Index: Int, CaseIterable, Identifiable {
case none = 0,
first = 1,
second = 2,
third = 3
var id: Self { self }
}
init(initialIndex: Int) {
self.initialIndex = initialIndex
_selection = State(initialValue: OrderPicker.Index(rawValue: initialIndex) ?? .none)
}
var body: some View {
Form {
Picker("Order in list", selection: $selection) {
ForEach(Index.allCases) { index in
Text(String(describing: index)).tag(index)
}
}
}
.frame(height: 116)
}
func getOrderIndex() -> Int {
let index = selection.rawValue
return index
}
}
Here is an approach for you:
struct ContentView: View {
#State private var selection: PickerType = PickerType.none
var body: some View {
OrderPicker(selection: $selection)
}
}
struct OrderPicker: View {
#Binding var selection: PickerType
var body: some View {
Form {
Picker("Order in list", selection: $selection) {
ForEach(PickerType.allCases, id: \.self) { item in
Text(item.rawValue)
}
}
.pickerStyle(WheelPickerStyle())
.onChange(of: selection, perform: { newValue in
print(newValue.rawValue, newValue.intValue)
})
}
}
}
enum PickerType: String, CaseIterable {
case none, first, second, third
var intValue: Int {
switch self {
case .none: return 0
case .first: return 1
case .second: return 2
case .third: return 3
}
}
}

Animation not working when pass single object to the view in SwiftUI

I have a rather strange problem with animation on adding elements to list in SwiftUI. I have a simple model class:
struct ShoppingList: Identifiable, Equatable {
let id: UUID
var name: String
var icon: String
var items: [Item]
init(id: UUID = UUID(), name: String, icon: String, items: [Item] = []) {
self.id = id
self.name = name
self.icon = icon
self.items = items
}
static func == (lhs: ShoppingList, rhs: ShoppingList) -> Bool {
return lhs.id == rhs.id
}
}
extension ShoppingList {
struct Item: Identifiable, Equatable {
let id: UUID
var name: String
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id
}
}
}
When I pass single ShoppingList object as binding to the view, adding animation basically doesn't happen at all:
Here is code of my view:
struct ShoppingListDetailView: View {
#Binding var shoppingList: ShoppingList
#State private var newItemName = ""
var body: some View {
List {
ForEach($shoppingList.items) { $item in
Text(item.name)
}
HStack {
TextField("Add item", text: $newItem)
Button(action: addNewItem) {
Image(systemName: "plus.circle.fill")
}
.disabled(newItemName.isEmpty)
}
}
.navigationTitle(shoppingList.name)
}
private func addNewItem() {
withAnimation {
let newItem = ShoppingList.Item(name: newItemName)
shoppingList.items.append(newItem)
}
}
}
And here is code of a parent view:
struct ShoppingListsView: View {
#Binding var shoppingLists: [ShoppingList]
var body: some View {
List {
ForEach($shoppingLists) { $list in
NavigationLink(destination: ShoppingListDetailView(shoppingList: $list)) {
ShoppingListItemView(shoppingList: $list)
}
}
}
.navigationTitle("Shopping List")
}
}
But once I pass whole list of ShoppingList objects and index for a particular one, everything works as expected:
Code with passing list to the view looks like that:
struct ShoppingListDetailView: View {
#Binding var shoppingList: [ShoppingList]
var index: Int
#State private var newItemName = ""
var body: some View {
List {
ForEach($shoppingList[index].items) { $item in
Text(item.name)
}
HStack {
TextField("Add item", text: $newItem)
Button(action: addNewItem) {
Image(systemName: "plus.circle.fill")
}
.disabled(newItemName.isEmpty)
}
}
.navigationTitle(shoppingList[index].name)
}
private func addNewItem() {
withAnimation {
let newItem = ShoppingList.Item(name: newItemName)
shoppingList[index].items.append(newItem)
}
}
}
And of course parent view:
struct ShoppingListsView: View {
#Binding var shoppingLists: [ShoppingList]
var body: some View {
List {
ForEach($shoppingLists) { $list in
NavigationLink(destination: ShoppingListDetailView(shoppingList: $shoppingLists, index: shoppingLists.firstIndex(of: list)!)) {
ShoppingListItemView(shoppingList: $list)
}
}
}
.navigationTitle("Shopping List")
}
}
I'm new to Swift (not to mention SwiftUI) and I have no idea what might be wrong here. Any ideas?

How to listen to a computed property in SwiftUI?

I am trying to disable a button based on a computed property from the View Model, but is only disabled after the view is reloaded.
This is the View Model :
class VerifyFieldViewModel : ObservableObject {
#ObservedObject var coreDataViewModel = CoreDataViewModel()
func isValidFirstName() -> Bool {
guard coreDataViewModel.savedDetails.first?.firstName?.count ?? 0 > 0 else {
return false
}
return true
}
func isValidLastName() -> Bool {
guard coreDataViewModel.savedDetails.first?.lastName?.count ?? 0 > 0 else {
return false
}
return true
}
var isFirstNameValid : String {
if isValidFirstName() {
return ""
} else {
return "Name is empty"
}
}
var isLastNameValid : String {
if isValidLastName() {
return ""
} else {
return "Surname is empty"
}
}
var isSignUpComplete: Bool {
if !isValidFirstName() || !isValidLastName() {
return false
}
return true
}
}
This is how I am disabling the button .
struct CartsView: View {
#State var onboardingState: Int = 0
#StateObject var coreDataViewModel = CoreDataViewModel()
#ObservedObject var verifyFieldViewModel = VerifyFieldViewModel()
var body: some View {
ZStack {
switch onboardingState {
case 0 :
VStack {
detailOrder
.transition(transition)
Spacer()
bottomButton
.padding(30)
}
case 2 :
VStack {
detailOrder2
.transition(transition)
Spacer()
bottomButton
.padding(30)
.opacity(verifyFieldViewModel.isSignUpComplete ? 1 : 0.6)
.disabled(!verifyFieldViewModel.isSignUpComplete)
}
default:
EmptyView()
}
}
}
}
This is the Core Data View Model :
class CoreDataViewModel : ObservableObject {
let manager = CoreDataManager.instance
#Published var savedDetails : [Details] = []
init() {
fetchSavedDetails()
}
func fetchSavedDetails() {
let request = NSFetchRequest<Details>(entityName: "Details")
do {
savedDetails = try manager.context.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
func saveContext() {
DispatchQueue.main.async {
self.manager.save()
self.fetchSavedDetails()
}
}
}
NOTE : It works, but only when the view is reloaded.
EDITED : I updated the question to make it easier to understand. Hope that you can help me now.
EDITED2: Added Core Data View Model .
As mentioned above you don't need a computed property in this case. I made a small example of Login procedure which demonstrates the same behavior.
class LoginViewModel: ObservableObject {
#Published var username: String = ""
#Published var password: String = ""
var isValid: Bool {
(username.isNotEmpty && password.isNotEmpty)
}
func login() {
// perform login
}
}
struct ContentView: View {
#StateObject private var vm: LoginViewModel = LoginViewModel()
var body: some View {
Form {
TextField("User name", text: $vm.username)
TextField("Password", text: $vm.password)
Button("Login") {
vm.login()
}.disabled(!vm.isValid)
}
}
}

SwiftUI #AppStorage doesn't refresh in a function

I am trying to reload the data every .onAppear, but if I change the #AppStorage nearMeter's value in the SettingsView, it isn't updating the value in the reloadNearStops func and using the previous #AppStorage value.
struct SettingsView: View {
#AppStorage(“nearMeter”) var nearMeter: Int = 1
#State var meters = ["100 m","200 m","400 m","500 m","750 m","1 km"]
var body: some View {
………
Picker(selection: $nearMeter, label: HStack {
Text(NSLocalizedString(“near_stops_distance”, comment: ""))
}) {
ForEach(0 ..< meters.count, id: \.self) {
Text(meters[$0])
}
}}}
struct FavouritesView: View {
#AppStorage(“nearMeter”) var nearMeter: Int = 1
func reloadNearStops(nearMeter: Int) {
print(nearMeter)
readNearStopsTimeTable.fetchTimeTable(nearMeter: getLonLatSpan(nearMeter: nearMeter), lat: (locationManager.lastLocation?.coordinate.latitude)!, lon: (locationManager.lastLocation?.coordinate.longitude)!)
}
func getLonLatSpan(nearMeter: Int) -> Double {
let meters = [100,200,400,500,750,1000]
if nearMeter < meters.count {
return Double(meters[nearMeter]) * 0.00001
}
else {
return 0.001
}
}
var body: some View {
.....
……….
.onAppear() {
if locationManager.lastLocation?.coordinate.longitude != nil {
if hasInternetConnection {
reloadNearStops(nearMeter: nearMeter)
}
}
}}
AppStorage won't call a function but onChange can call a function when AppStorage has changed.
struct StorageFunctionView: View {
#AppStorage("nearMeter") var nearMeter: Int = 1
#State var text: String = ""
var body: some View {
VStack{
Text(text)
Button("change-storage", action: {
nearMeter = Int.random(in: 0...100)
})
}
//This will listed for changes in AppStorage
.onChange(of: nearMeter, perform: { newNearMeter in
//Then call the function and if you need to pass the new value do it like this
fetchSomething(value: newNearMeter)
})
}
func fetchSomething(value: Int) {
text = "I'm fetching \(value)"
}
}

Picker on select

I am trying to run a function after a user has selected a picker option.
The idea is that the user can set a default station, so I need to be able to send the selected value to a function so I can save it inside the core data module. How can I achieve this?
import SwiftUI
struct SettingsView: View {
var frameworks = ["DRN1", "DRN1 Hits", "DRN1 United", "DRN1 Life"]
#State private var selectedFrameworkIndex = 0
func doSomethingWith(value: String) {
print(value)
}
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedFrameworkIndex, label: Text("Favorite Station")) {
ForEach(0 ..< frameworks.count) {
Text(self.frameworks[$0])
}
}.onReceive([self.frameworks].publisher.first()) { value in
self.doSomethingWith(value: value)
}
}
}
.navigationBarTitle("Settings")
}
}
}
Instead of using onRecive try using onChange.
Your code:
.onReceive([self.frameworks].publisher.first()) { value in
self.doSomethingWith(value: value)
}
Correction:
.onChange(of: $selectedFrameworkIndex, perform: { value in
self.doSomethingWith(value: value)
})
this will trigger every time $selectedFrameworkIndex is changed.
I would use a simple .onChange(of:) call on the picker. It will pick up on the change of the #State var and allow you to act on it. Use it like this:
import SwiftUI
struct SettingsView: View {
var frameworks = ["DRN1", "DRN1 Hits", "DRN1 United", "DRN1 Life"]
#State private var selectedFrameworkIndex = 0
func doSomethingWith(value: String) {
print(value)
}
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedFrameworkIndex, label: Text("Favorite Station")) {
ForEach(0 ..< frameworks.count) {
Text(self.frameworks[$0])
}
}
// Put your function call in here:
.onChange(of: selectedFrameworkIndex) { value in
print(value)
}
}
}
}
.navigationBarTitle("Settings")
}
}
}
yours:
import SwiftUI
struct SettingsView: View {
var frameworks = ["DRN1", "DRN1 Hits", "DRN1 United", "DRN1 Life"]
#State private var selectedFrameworkIndex = 0
func doSomethingWith(value: String) {
print(value)
}
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedFrameworkIndex, label: Text("Favorite Station")) {
ForEach(0 ..< frameworks.count) {
Text(self.frameworks[$0])
}
}.onReceive([self.frameworks].publisher.first()) { value in
self.doSomethingWith(value: value)
}
}
}
.navigationBarTitle("Settings")
}
}
}
add a variable that checks to see if selectedFrameworkIndex has changed.
change:
import SwiftUI
struct SettingsView: View {
var frameworks = ["DRN1", "DRN1 Hits", "DRN1 United", "DRN1 Life"]
#State private var selectedFrameworkIndex = 0
#State private var savedFrameworkIndex = 0
func handler<Value: Any>(val: Value) -> Value {
if selectedFrameworkIndex != savedFrameworkIndex {
self.doSomethingWith(value: self.frameworks[selectedFrameworkIndex])
}
return val
}
func doSomethingWith(value: String) {
print(value)
}
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: handler(val: $selectedFrameworkIndex), label: Text("Favorite Station")) {
ForEach(0 ..< frameworks.count) {
Text(self.frameworks[$0])
}
}
}
}
.navigationBarTitle("Settings")
}
}
}