SwiftUI: How to combine .toggle() not Toggle(isOn..) with UserDefault - swiftui

Currently I have
#Published var isVisited: Bool {
didSet{
UserDefaults.standard.set(isVisited,forKey: "isVisited")
}
}
init() {
self.isVisited = UserDefaults.standard.object(forKey: "isVisited") as? Bool ?? false
}
#State private var edit = false
#ObservedObject var userSettings = UserSettings()
Button(action: { self.edit.toggle()}) {
if self.edit {
Image(systemName: "heart.fill").font(.title)
} else {
Image(systemName: "heart").font(.title)
}
}.foregroundColor(.red)
Ideally, whenever the "heart" icon is tapped, it will be filled and save data to UserDefault database. I only know the uses of Toggle(isOn:..).
Any suggestions would be much appreciated!

Do you mean something like this?
class UserSettings: ObservableObject {
init() {
isVisited = UserDefaults.standard.bool(forKey: "isVisited")
}
#Published var isVisited: Bool {
didSet {
UserDefaults.standard.set(isVisited, forKey: "isVisited")
}
}
}
struct RootView: View {
#ObservedObject var userSettings = UserSettings()
var body: some View {
Button(action: { self.userSettings.isVisited.toggle() }) {
if self.userSettings.isVisited {
Image(systemName: "heart.fill").font(.title)
} else {
Image(systemName: "heart").font(.title)
}
}.foregroundColor(.red)
}
}
What I did here is that every time UserSettings loads I load isVisited from UserDefaults and then whenever that property is set it updates UserDefaults.
Then on the view instead of using #State to hold the data I'm getting the data directly from UserSettings, since it will have the most updated data.

Related

How to force List to redraw by another View's toggle Button?

I fetched JSON data from Google Sheet and populate into a List using ForEach. I used struct HeaderView located in another View and place a Button to serve as a toggle. However, the List will not redraw when I press the toggle button even I use #State ascd variable.
Below is some of my code, is there anything I miss?
struct HeaderView: View {
// #State var asc: Bool = true
var holding: String = "持倉"
var earning: String = "賺蝕"
// #State var tog_value: Bool = ContentView().ascd
var body: some View {
HStack {
Button(action: {
ContentView().ascd.toggle()
}
) {
Text("Button")
}
Text(holding)
Text(earning)
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
#ObservedObject var viewModelTotal = ContentViewModelTotal()
#State var ascd: Bool = false
var totalss = ContentViewModelTotal.fetchDatasTotal
var body: some View {
List {
Section(header: HeaderView()) {
ForEach(viewModel.rows, id: \.stockname) { rows in
// Text(user.stock_name)
ListRow(name: rows.stockname, code: rows.stockcode, cur_price: rows.currentprice, mkt_value: rows.marketvalue, amnt: rows.amount, avg_cost: rows.averagecost, pft: rows.profit, pft_pcnt: rows.profitpercent)
}
}
.onAppear {
self.viewModel.fetchDatas()
self.ascd.toggle()
if self.ascd {
self.viewModel.rows.sort { $0.stockname < $1.stockname }
} else {
self.viewModel.rows.sort { $0.stockname > $1.stockname }
}
}
}
}
}
For changing another View's variable you can use a #Binding variable:
struct HeaderView: View {
...
#Binding var ascd: Bool
var body: some View {
HStack {
Button(action: {
self.ascd.toggle()
}) {
Text("Button")
}
Text(holding)
Text(earning)
}
}
}
I'd recommend moving sorting logic to your ViewModel.
class ContentViewModel: ObservableObject {
#Published var ascd: Bool = false {
didSet {
if ascd {
rows.sort { $0.hashValue < $1.hashValue }
} else {
rows.sort { $0.hashValue > $1.hashValue }
}
}
}
...
}
If it's in the .onAppear in the ContentView it will be executed only when your View is shown on the screen.
And you will have to initialise your HeaderView with your ViewModel's ascd variable:
HeaderView(ascd: $viewModel.ascd)

How to swap my #State of my SwiftUI view for my view model #Published variable?

I have a button that triggers my view state. As I have now added a network call, I would like my view model to replace the #State with its #Publihed variable to perform the same changes.
How to use my #Published in the place of my #State variable?
So this is my SwiftUI view:
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
// This is the value I want to use as #Publisher
#State var isLoggedIn = false
var body: some View {
ZStack {
Button(action: {
// Before my #State was here
// self.isLoggedIn = true
self.viewModel.login()
}) {
Text("Log in")
}
if isLoggedIn {
TutorialView()
}
}
}
}
And this is my model:
final class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
private var subscriptions = Set<AnyCancellable>()
func demoLogin() {
AuthRequest.shared.login()
.sink(
receiveCompletion: { print($0) },
receiveValue: {
// My credentials
print("Login: \($0.login)\nToken: \($0.token)")
DispatchQueue.main.async {
// Once I am logged in, I want this
// value to change my view.
self.isLoggedIn = true } })
.store(in: &subscriptions)
}
}
Remove state and use view model member directly, as below
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
self.viewModel.demoLogin()
}) {
Text("Log in")
}
if viewModel.isLoggedIn { // << here !!
TutorialView()
}
}
}
}
Hey Roland I think that what you are looking for is this:
$viewMode.isLoggedIn
Adding the $ before the var will ensure that SwiftUI is aware of its value changes.
struct ContentView: View {
#ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
ZStack {
Button(action: {
viewModel.login()
}) {
Text("Log in")
}
if $viewMode.isLoggedIn {
TutorialView()
}
}
}
}
class OnboardingViewModel: ObservableObject {
#Published var isLoggedIn = false
func login() {
isLoggedIn = true
}
}

How to add an observable property when other properties change

I have the following model object that I use to populate a List with a Toggle for each row, which is bound to measurement.isSelected
final class Model: ObservableObject {
struct Measurement: Identifiable {
var id = UUID()
let name: String
var isSelected: Binding<Bool>
var selected: Bool = false
init(name: String) {
self.name = name
let selected = CurrentValueSubject<Bool, Never>(false)
self.isSelected = Binding<Bool>(get: { selected.value }, set: { selected.value = $0 })
}
}
#Published var measurements: [Measurement]
#Published var hasSelection: Bool = false // How to set this?
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'd like the hasSelection property to be true whenever any measurement.isSelected is true. I'm guessing somehow Model needs to observe changes in measurements and then update its hasSelection property… but I've no idea where to start!
The idea is that hasSelection will be bound to a Button to enable or disable it.
Model is used as follows…
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: $model.hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
var body: some View {
HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: measurement.isSelected)
.labelsHidden()
}
}
}
For info, here's a screenshot of what I'm trying to achieve. A list of selectable items, with a navigation link that is enabled when one or more is selected, and disabled when no items are selected.
#user3441734 hasSelection should ideally be a get only property, that
is true if any of measurement.isSelected is true
struct Data {
var bool: Bool
}
class Model: ObservableObject {
#Published var arr: [Data] = []
var anyTrue: Bool {
arr.map{$0.bool}.contains(true)
}
}
example (as before) copy - paste - run
import SwiftUI
struct Data: Identifiable {
let id = UUID()
var name: String
var on_off: Bool
}
class Model: ObservableObject {
#Published var data = [Data(name: "alfa", on_off: false), Data(name: "beta", on_off: false), Data(name: "gama", on_off: false)]
var bool: Bool {
data.map {$0.on_off} .contains(true)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}
Text("\(model.bool.description)").font(.largeTitle).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When the model.data is updated
#Published var data ....
its publisher calls objectWillChange on ObservableObject.
Next SwiftUI recognize that ObservedObject needs the View to be "updated". The View is recreated, and that will force the model.bool.description will have fresh value.
LAST UPDATE
change this part of code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}.navigationBarTitle("List")
.navigationBarItems(trailing:
NavigationLink(destination: Text("next"), label: {
Text("Next")
}).disabled(!model.bool)
)
}
}
}
and it is EXACTLY, WHAT YOU HAVE in your updated question
Try it on real device, otherwise the NavigationLink is usable only once (this is well known simulator bug in current Xcode 11.3.1 (11C504)).
The problem with your code at the moment is that even if you observe the changes to measurements, they will not get updated when the selection updates, because you declared the var isSelected: Binding<Bool> as a Binding. This means that SwiftUI is storing it outside of your struct, and the struct itself doesn't update (stays immutable).
What you could try instead is declaring #Published var selectedMeasurementId: UUID? = nil on your model So your code would be something like this:
import SwiftUI
import Combine
struct NextView: View {
var body: some View {
Text("Next View")
}
}
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
let hasSelection = Binding<Bool> (
get: {
self.model.selectedMeasurementId != nil
},
set: { value in
self.model.selectedMeasurementId = nil
}
)
return NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement, selectedMeasurementId: self.$model.selectedMeasurementId)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
#Binding var selectedMeasurementId: UUID?
var body: some View {
let isSelected = Binding<Bool>(
get: {
self.selectedMeasurementId == self.measurement.id
},
set: { value in
if value {
self.selectedMeasurementId = self.measurement.id
} else {
self.selectedMeasurementId = nil
}
}
)
return HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: isSelected)
.labelsHidden()
}
}
}
final class Model: ObservableObject {
#Published var selectedMeasurementId: UUID? = nil
struct Measurement: Identifiable {
var id = UUID()
let name: String
init(name: String) {
self.name = name
}
}
#Published var measurements: [Measurement]
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'm not sure exactly how you want the navigation button in the navbar to behave. For now I just set the selection to nil when it's tapped. You can modify it depending on what you want to do.
If you want to support multi-selection, you can use a Set of selected ids instead.
Also, seems like the iOS simulator has some problems with navigation, but I tested on a physical device and it worked.

Refreshing NavigationView Environment Object

Im trying to create an environment object that is editable and putting it in a list.
The Variables are only refreshing when I switch the tab for example (so whenever I leave the NavigationView) and then come back.
The same worked with a ModalView before. Is it a bug maybe? Or am I doing something wrong?
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
try passing .environmentObject on your destination:
NavigationLink(destination: detailView(feed: self._feed, i: i).environmentObject(x))
You have to use willSet instead of didSet.
TestItem should be a value type: struct or enum. SwiftUI's observation system properly works only with value types.

Refreshing a SwiftUI List

Ím trying to refresh this List whenever I click on a NavLink
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: ListFeedItemDetail(idx: i).environmentObject(self.feed)) {
ListFeedItem(item: self.$feed.items[i])
}
}
}
The list is made out of an array inside an environment object.
The problem: It does only refresh when I switch to another tab or close the app
I had used a modal View before and it worked there. (I did it with .onAppear)
Any Ideas?
Example
Problem: When you tap on an item in the list and tap the toggle button the EnvironmentObject is changed but this changed is only reflected when I change the tab and change it back again
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
I had a similar problem, this is the hack I came up with.
In your "TestView" declare:
#State var needRefresh: Bool = false
Pass this to your "detailView" destination, such as:
NavigationLink(destination: detailView(feed: self._feed, i: i, needRefresh: self.$needRefresh)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}.accentColor(self.needRefresh ? .white : .black)
}
Note ".accentColor(self.needRefresh ? .white : .black)" to force a refresh when "needRefresh"
is changed.
In your "detailView" destination add:
#Binding var needRefresh: Bool
Then in your "detailView" in your Button action, add:
self.needRefresh.toggle()