SwiftUI toggle switches - swiftui

I'm trying to implement a simple toggle switch but I'm having trouble saving the new toggle/switch status as when I change views and go back into the setting, it's defaulted back to the off switch. Can you tell me what I'm doing wrong?
struct StudyMode: View {
#State private var overdueFirst = UserDefaults.standard.bool(forKey: "Overdue First")
#EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
HStack {
Toggle(isOn: $overdueFirst) {
Text("Overdue cards first")
}
.onTapGesture {
var newSwitch = false
if self.overdueFirst == false {
newSwitch = true
}
UserDefaults.standard.set(newSwitch, forKey: "Overdue First")
}
}
Spacer()
Text("By enabling this option, the cards will be ordered such that you will revise all overdue cards before you start learning new words.")
.font(.system(size: 12))
}
}
}

Your .onTapGesture over Toggle is not called, because latter does not allow it (handling tap by itself, and even .simultaneousGesture will not help)
Here is possible approach to achieve the goal (tested with Xcode 11.2 / iOS 13.2)
...
// define proxy binding, wrapping direct work with UserDefaults
private let overdueFirst = Binding<Bool>(
get: { UserDefaults.standard.bool(forKey: "Overdue First") },
set: { UserDefaults.standard.set($0, forKey: "Overdue First") })
var body: some View {
VStack {
HStack {
Toggle(isOn: overdueFirst) { // use proxy binding directly
Text("Overdue cards first")
}
}
...
However I would recommend to bind this Toggle directly with your UserSettings corresponding property and handle sync with UserDefaults there (eg. on didSet).

Related

Navigate to another view when async/await task finishes

I'm pretty new to SwiftUI, so I'm wondering how to navigate to a new view only when an async method returns with a value. Here is my view:
import SwiftUI
struct FollowersView: View {
#State var followers: [Follower]
#State var user: User?
#State private var selection: String? = nil
#StateObject private var viewModel = GitHubUsersViewModel()
let columns = [
GridItem(.flexible(minimum: 150, maximum: 175)),
GridItem(.flexible(minimum: 150, maximum: 175)),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(followers, id: \.self) { follower in
FollowerCardView(follower: follower)
.onTapGesture {
Task {
self.user = try await self.viewModel.manager.getUser(for: follower.login)
self.selection = NavigationTags.userFound
}
}
}
}
.padding(.horizontal)
}
.frame(maxHeight: .infinity)
}
}
When self.user gets populated, I want to be able to navigate to another view. But, I can't figure out where to put the NavigationLink.
This FollowersView is already embedded in a NavigationView from within it's parent view.
Please advise?
For iOS 15 you can still use NavigationLink as it isn't deprecated until iOS 16 when the new NavigationStack stuff comes in.
Anyway...
struct SomeView: View {
#State var shouldNavigate: Bool = false
var body: some View {
NavigationLink(
isActive: $shouldNavigate,
destination: navDestination
) {
Text("Press me")
.onTapGesture {
let result = await someAsyncFunction()
shouldNavigate = true
}
}
}
#ViewBuilder
var navDestination: some View {
Text("Ta dah!")
}
}
This will navigate to your destination when the async function completes and sets shouldNavigate to true.
There are nmany ways that you could do this. This is just one of them. Good luck
As a side note, you need to make sure your view is part of a NavigationView too.

SwiftUI NavigationLink with constant binding for isActive

I don't understand why SwiftUI NavigationLink's isActive behaves as if it has it's own state. Even though I pass a constant to it, the back button overrides the value of the binding once pressed.
Code:
import Foundation
import SwiftUI
struct NavigationLinkPlayground: View {
#State
var active = true
var body: some View {
NavigationView {
VStack {
Text("Navigation Link playground")
Button(action: { active.toggle() }) {
Text("Toggle")
}
Spacer()
.frame(height: 40)
FixedNavigator(active: active)
}
}
}
}
fileprivate struct FixedNavigator: View {
var active: Bool = true
var body: some View {
return VStack {
Text("Fixed navigator is active: \(active)" as String)
NavigationLink(
destination: SecondScreen(),
// this is technically a constant!
isActive: Binding(
get: { active },
set: { newActive in print("User is setting to \(newActive), but we don't let them!") }
),
label: { Text("Go to second screen") }
)
}
}
}
fileprivate struct SecondScreen: View {
var body: some View {
Text("Nothing to see here")
}
}
This is a minimum reproducible example, my actual intention is to handle the back button press manually. So when the set inside the Binding is called, I want to be able to decide when to actually proceed. (So like based on some validation or something.)
And I don't understand what is going in and why the back button is able to override a constant binding.
Your use of isActive is wrong. isActive takes a binding boolean and whenever you set that binding boolean to true, the navigation link gets activated and you are navigated to the destination.
isActive does not control whether the navigation link is clickable/disbaled or not.
Here's an example of correct use of isActive. You can manually trigger the navigation to your second view by setting activateNavigationLink to true.
EDIT 1:
In this new sample code, you can disable and enable the back button at will as well:
struct ContentView: View {
#State var activateNavigationLink = false
var body: some View {
NavigationView {
VStack {
// This isn't visible and should take 0 space from the screen!
// Because its `label` is an `EmptyView`
// It'll get programmatically triggered when you set `activateNavigationLink` to `true`.
NavigationLink(
destination: SecondScreen(),
isActive: $activateNavigationLink,
label: EmptyView.init
)
Text("Fixed navigator is active: \(activateNavigationLink)" as String)
Button("Go to second screen") {
activateNavigationLink = true
}
}
}
}
}
fileprivate struct SecondScreen: View {
#State var backButtonActivated = false
var body: some View {
VStack {
Text("Nothing to see here")
Button("Back button is visible: \(backButtonActivated)" as String) {
backButtonActivated.toggle()
}
}
.navigationBarBackButtonHidden(!backButtonActivated)
}
}

SwiftUI List rows with INFO button

UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}

swiftUI : Updating Text after changes in settings

[Edit(1) to reflect posting of streamlined app to illustrate the issue : ].
[Edit (2) : completely removed EnvironmentObject and app now works ! Not understanding WHY body is refreshed as NO #State vars are being modified...Code at end of text]
I am writing an app that, at some point, displays some text, related to the contents of 2 Arrays, depending on a set of rules. These rules can be set in a Settings view, as User's preference.
So, when a user changes the rules he wants applied in Settings, that text needs to be re-assessed.
But of course, things aren't that easy.
I present my settings view as modal on my main ContentView, and when I dismiss that modal, the body of the ContentView is not redrawn...
I created an EnvironmentObject with #Published vars in order to keep track of all the user preferences (that are also written to UserDefaults), and shared that #EnvironmentObject with both my ContentView and SettingsView, in the hope that, being an observedObject, its changes would trigger a refresh of my ContentView.
Not so...
Any ideas to help me go forward on this ? Any pointers would be greatly appreciated (again!).
Posted app on GitHub has following architecture :
An appState EnvironmentObject,
A ContentView that displays a set of texts, depending on some user preferences set in
A settingsView
UserDefaults are initialized in AppDelegate.
Thanks for any help on this...
Content view :
import SwiftUI
struct ContentView: View {
#State var modalIsPresented = false // The "settingsView" modally presented as a sheet
#State private var modalViewCaller = 0 // This triggers the appropriate modal (only one in this example)
var body: some View {
NavigationView {
VStack {
Spacer()
VStack {
Text(generateStrings().text1)
.foregroundColor(Color(UIColor.systemGreen))
Text(generateStrings().text2)
} // end of VStack
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(nil) // allows unlimited lines
.padding(.all)
Spacer()
} // END of main VStack
.onAppear() {
self.modalViewCaller = 0
}
.navigationBarTitle("Test app", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 6 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.sheet(isPresented: $modalIsPresented, content: sheetContent)
.navigationViewStyle(StackNavigationViewStyle()) // This avoids dual column on iPad
} // END of var body: some View
// MARK: #ViewBuilder func sheetContent() :
#ViewBuilder func sheetContent() -> some View {
if modalViewCaller == 6 {
SettingsView()
}
} // END of func sheetContent
// MARK: generateStrings() : -
func generateStrings() -> (text1: String, text2: String, recapText: String, isHappy: Bool) { // minimumNumberOfEventsCheck
var myBool = false
var aString = "" // The text 1 string
var bString = "" // The text 2 string
var cString = "" // The recap string
if UserDefaults.standard.bool(forKey: kmultiRules) { // The user chose the dual rules option
let ruleSet = UserDefaults.standard.integer(forKey: kruleSelection) + 1
aString = "User chose 2 rules option"
bString = "User chose rule set # \(ruleSet)"
myBool = true
print("isDualRules true loop : generateStrings was called at \(Date().debugDescription)")
cString = "Dual rules option, user chose rule set nb \(ruleSet)"
}
else // The user chose the single rule option
{
aString = "User chose single rule option"
bString = "User had no choice : there is only one set of rules !"
myBool = false
print("isDualRules false loop : generateStrings was called at \(Date().debugDescription)")
cString = "Single rule option, user chose nothing."
}
return (aString, bString, cString, myBool)
} // End of func generatestrings() -> String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}
SettingsView :
import SwiftUI
import UIKit
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#State public var multiRules = UserDefaults.standard.bool(forKey: kmultiRules)
#State private var ruleSelection = UserDefaults.standard.integer(forKey: kruleSelection) // 0 is rule 1, 1 is rule 2
var body: some View {
NavigationView {
List {
Toggle(isOn: $multiRules)
{
Text("more than one rule ?")
}
.padding(.horizontal)
if multiRules {
Picker("", selection: $ruleSelection){
Text("rules 1").tag(0)
Text("rules 2").tag(1)
}.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
}
} // End of List
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We try to save once more if needed
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
}
)
.navigationBarTitle("Settings", displayMode: .inline)
} // END of Navigation view
} // END of some View
func saveDefaults() {
UserDefaults.standard.set(multiRules, forKey: kmultiRules)
UserDefaults.standard.set(ruleSelection, forKey: kruleSelection)
}
}
// MARK: Preview struct
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
return SettingsView()
}
}
Constants.swift file :
import Foundation
import SwiftUI
let kmultiRules = "two rules"
let kruleSelection = "rules selection"
let kappStateChanged = "appStateChanged"
AppDelegate :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.standard.register(defaults: [ // We initialize the UserDefaults
"two rules": false,
"rules selection": 0, // 0 is ruel 1, 1 is rule 2
"appStateChanged": false
])
return true
}
If you have a shared #EnvironmentObject with #Published properties in two views, if you change such a property from one view, the other one will be re-execute the body property and the view will be updated.
It really helps to create simple standalone examples - not only for asking here, also for gaining a deeper understanding / getting an idea why it doesn't work in the complex case.
For example:
import SwiftUI
class TextSettings: ObservableObject {
#Published var count: Int = 1
}
struct TextSettingsView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Form {
Picker(selection: $settings.count, label:
Text("Text Repeat Count"))
{
ForEach(Array(1...5), id: \.self) { value in
Text(String(value)).tag(value)
}
}
}
}
}
struct TextWithSettingExampleView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Text(String(repeating: "Hello ", count: Int(settings.count)))
.navigationBarItems(trailing: NavigationLink("Settings", destination: TextSettingsView()))
}
}
struct TextWithSettingExampleView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TextWithSettingExampleView()
}
.environmentObject(TextSettings())
}
}
Not sure I fully understand the question, but I had what I believe might be a similar problem where I never got my contentview to reflect the updates in my observed object when the changes were triggered from a modal. I solved/hacked this by triggering an action in my observed object when dismissing the modal like this:
struct ContentView: View {
//
#State var isPresentingModal = false
var body: some View {
//
.sheet(isPresented: self.$isPresentingModal) {
PresentedModalView()
.onDisappear {
//Do something here
}
}
}
}

Is it possible for a NavigationLink to perform an action in addition to navigating to another view?

I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.