SwiftUI Conditionally trigger a toggle - swift

I want the user can only click the toggle under some specific condition. So I reverse the value of "AutocorrectStatus" again under .onChange method. But it seems like the view doesn't follow this change. It still becomes on from off even the value of AutocorrectStatus is false. What should I do?
class GlobalEnvironment: ObservableObject {
#Published var AutocorrectStatus = false
}
struct SettingView: View {
#EnvironmentObject var env: GlobalEnvironment
HStack() {
Toggle("", isOn: self.$env.AutocorrectStatus)
.labelsHidden()
.onChange(of: self.env.AutocorrectStatus) { _AutocorrectStatus in
self.env.AutocorrectStatus = !self.env.AutocorrectStatus
}
if self.env.AutocorrectStatus {
Text ("ON")
.font(.system(size: 26, weight: .semibold))
.frame(alignment: .topLeading)
} else {
Text ("OFF")
.font(.system(size: 26, weight: .semibold))
.frame(alignment: .topLeading)
}
}
}

There are several ways to do this. One way is to provide the toggle a Binding that performs the necessary checks before updating the value.
var body: some View {
HStack {
Toggle("", isOn: self.provideAutocorrectBinding())
}
}
func provideAutocorrectBinding() -> Binding<Bool> {
return Binding(get: {
return self.env.AutocorrectStatus
}, set: { newValue in
let isConnected = false // Your logic to check the connection
if isConnected {
self.env.AutocorrectStatus = newValue
}
})
}
You can trigger the alert there as well:
struct ContentView: View {
#EnvironmentObject var env: GlobalEnvironment
#State private var showingAlert = false
var body: some View {
HStack {
Toggle("", isOn: self.provideAutocorrectBinding())
}
.alert("Your message.", isPresented: $showingAlert) {
Button("OK", role: .cancel) { }
}
}
func provideAutocorrectBinding() -> Binding<Bool> {
return Binding(get: {
return self.env.AutocorrectStatus
}, set: { newValue in
let isConnected = false // Your logic to check the connection
if isConnected {
self.env.AutocorrectStatus = newValue
} else {
self.showingAlert.toggle()
}
})
}
}

Related

Button with NavigationLink and function call SwiftUI

I have an issue with NavigationLinks with conditions. It doesn't react what I'm expected. When a user click on the button the function "test" must be called and give a return value. If the return value is true the "SheetView" must be openend directly without clicking on the NavigationLink text. Please could someone give me a help on this one. Thanks in advance
I made a small (sample) program for showing the issue.
import SwiftUI
struct LoginView: View {
#State var check = false
#State var answer = false
var body: some View {
NavigationView {
VStack{
Text("it doesn't work")
Button(action: {
answer = test(value: 2)
if answer {
//if the return value is true then directly navigate to the sheetview
NavigationLink(
destination: SheetView(),
label: {
Text("Navigate")
})
}
}, label: {
Text("Calculate")
})
}
}
}
func test(value: Int) -> Bool {
if value == 1 {
check = false
} else {
print("Succes")
check = true
}
return check
}
}
struct SheetView: View {
var body: some View {
NavigationView {
VStack{
Text("Test")
.font(.title)
}
}
}
}
The answer from Yodagama works if you were trying to present a sheet (because you called your navigation destination SheetView), but if you were trying to navigate to SheetView instead of present a sheet, the following code would do that.
struct LoginView: View {
#State var check = false
#State var answer = false
var body: some View {
NavigationView {
VStack{
Text("it doesn't work")
NavigationLink(destination: SheetView(), isActive: $answer, label: {
Button(action: {
answer = test(value: 2)
}, label: {
Text("Calculate")
})
})
}
}
}
func test(value: Int) -> Bool {
if value == 1 {
check = false
} else {
print("Succes")
check = true
}
return check
}
}
struct SheetView: View {
var body: some View {
NavigationView {
VStack{
Text("Test")
.font(.title)
}
}
}
}
struct LoginView: View {
#State var check = false
#State var answer = false
var body: some View {
NavigationView {
VStack{
Text("it doesn't work")
Button(action: {
answer = test(value: 2)
//<= here
}, label: {
Text("Calculate")
})
}
.sheet(isPresented: $answer, content: { //<= here
SheetView()
})
}
}
...

How to create fully customizable sections with Binding text?

First of all, sorry for the title which is not precise at all, but I didn't know how else I could title this question. So:
1. What I want to achieve:
I want to create an interface where the user can add sections to input different fields, which are fully customizable by the user himself.
2. What I have done so far
I was able to create the interface, I can add new sections easily (structured with a "Section Name" on top and a TextField below) and they are customizable, but only in the TextField. They are also deletable, even though I had to do a complicated workaround since the Binding text of the TextField caused the app to crash because the index at which I was trying to remove the item resulted as "out of range". It's not the perfect workaround, but it works, and for now this is the most important thing. When I'll save these sections, I'll save them as an array of Dictionaries where every Dictionary has the section name and its value. However, there's still a few things I wasn't able to do:
3. What I haven't done yet
There are still 3 things that I couldn't do yet.
First of all, I'd like for the name of the section to be editable.
Secondly, I'd like to have the sections that the user adds displayed inside a Form and divided by Sections. As header, I'd like to have each different section name, and grouped inside all the related sections that share the same name.
Last but not least, I'd like to allow the user to add multiple TextFields to the same section. I have no idea how to handle this or even if it's possible.
4. Code:
ContentView:
import SwiftUI
struct ContentView: View {
#State var editSections = false
#State var arraySections : [SectionModel] = [SectionModel(name: "Prova", value: ""), SectionModel(name: "Prova 2", value: ""), SectionModel(name: "Prova", value: "")]
#State var showProgressView = false
#State var arraySectionsForDeleting = [SectionModel]()
#State var openSheetAdditionalSections = false
var body: some View {
Form {
if showProgressView == false {
if editSections == false {
ForEach(arraySections, id: \.id) { sect in
Section(header: Text(sect.name)) {
ForEach(arraySections, id: \.id) { sez in
if sez.name == sect.name {
SectionView(section: sez)
}
}
}
}
} else {
forEachViewSectionsForDeleting
}
if arraySections.count > 0 {
buttoneditSections
}
} else {
loadingView
}
Section {
addSections
}
}
.sheet(isPresented: $openSheetAdditionalSections, content: {
AdditionalSectionsSheet(closeSheet: $openSheetAdditionalSections, contView: self)
})
}
var forEachViewSectionsForDeleting : some View {
Section {
ForEach(arraySections, id: \.id) { sez in
HStack {
Text("\(sez.name) - \(sez.value)")
.foregroundColor(.black)
Spacer()
Button(action: {
showProgressView = true
let idx = arraySections.firstIndex(where: { $0.id == sez.id })
arraySectionsForDeleting.remove(at: idx!)
arraySections = []
arraySections = arraySectionsForDeleting
showProgressView = false
}, label: {
Image(systemName: "minus.circle")
.foregroundColor(.yellow)
}).buttonStyle(BorderlessButtonStyle())
}
}
}
}
var buttoneditSections : some View {
Button(action: {
editSections.toggle()
}, label: {
Text(editSections == true ? "Done" : "Edit Sections")
.foregroundColor(.yellow)
})
}
var forEachviewSezioniNonModifica : some View {
Section {
ForEach(arraySections, id: \.id) { sez in
Text(sez.name)
.foregroundColor(.black)
Text(sez.value)
.foregroundColor(.black)
}
}
}
var addSections : some View {
Button(action: {
openSheetAdditionalSections = true
}, label: {
HStack {
Text("Add sections")
.foregroundColor(.yellow)
Spacer()
Image(systemName: "plus.circle")
.foregroundColor(.yellow)
}
})
}
var loadingView : some View {
Section {
HStack {
Spacer()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .black))
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddSectionSheet and SectionView:
import SwiftUI
struct AdditionalSectionsSheet: View {
#Binding var closeSheet : Bool
#Environment(\.colorScheme) var colorScheme
var contView : ContentView
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#GestureState private var dragOffset = CGSize.zero
var body: some View {
NavigationView {
Form {
buttonPhone
buttonUrl
buttonEmail
buttonAddress
}
.navigationBarTitle(Text("Add section"), displayMode: .inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: Button(action : {
closeSheet = false
}){
Text("Close")
.foregroundColor(.yellow)
})
}
}
var buttonPhone : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "Phone", value: ""))
contView.showProgressView = true
closeSheet = false
}, label: {
HStack {
Text("Phone")
.foregroundColor(.black)
Spacer()
}
})
}
var buttonUrl : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "URL", value: ""))
closeSheet = false
}, label: {
HStack {
Text("URL")
.foregroundColor(.black)
Spacer()
}
})
}
var buttonAddress : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "Address", value: ""))
contView.showProgressView = true
closeSheet = false
}, label: {
HStack {
Text("Address")
.foregroundColor(.black)
Spacer()
}
})
}
var buttonEmail : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "Email", value: ""))
contView.showProgressView = true
closeSheet = false
}, label: {
HStack {
Text("Email")
.foregroundColor(.black)
Spacer()
}
})
}
}
struct SectionView : View {
#Environment(\.colorScheme) var colorScheme
#ObservedObject var section : SectionModel
var body : some View {
Section {
Text(section.name)
.foregroundColor(.black)
TextField(section.name, text: self.$section.value)
.foregroundColor(.black)
}
}
}
SectionModel:
import SwiftUI
import Combine
class SectionModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var value : String
init(name: String, value: String) {
self.name = name
self.value = value
}
static func == (lhs: SectionModel, rhs: SectionModel) -> Bool {
true
}
static func < (lhs: SectionModel, rhs: SectionModel) -> Bool {
true
}
}
I hope I wrote things clear enough to be understood, thanks to everyone who will help!

SwiftUI Animation on property change?

I want to animate the appearance of an item in a list. The list looks like this:
Text("Jim")
Text("Jonas")
TextField("New Player")
TextField("New Player") //should be animated when appearing (not shown until a name is typed in the first "New Player")
The last TextField should be hidden until newPersonName.count > 0 and then appear with an animation.
This is the code:
struct V_NewSession: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
var body: some View {
VStack(alignment: .leading) {
ForEach(0...self.participants.count + 1, id: \.self) { i in
// Without this if statement, the animation works
// but the Textfield shouldn't be shown until a name is typed
if (!(self.newPersonName.count == 0 && i == self.participants.count + 1)) {
HStack {
if(i < self.participants.count) {
Text(self.participants[i])
} else {
TextField("New Player",
text: $newPersonName,
onEditingChanged: { focused in
if (self.newPersonName.count == 0) {
if (!focused) {
handleNewPlayerEntry()
}
}
})
}
}
}
}
.transition(.scale)
Spacer()
}
}
func handleNewPlayerEntry() {
if(newPersonName.count > 0) {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
}
}
}
}
I know withAnimation(...) only applies to participants.append(newPersonName), but how can I get the animation to work on the property change in the if-statement?
if ((!(self.newPersonName.count == 0 && i == self.participants.count + 1)).animation()) doesn't work.
Your example code won't compile for me, but here's a trivial example of using Combine inside a ViewModel to control whether a second TextField appears based on a condition:
import SwiftUI
import Combine
class ViewModel : ObservableObject {
#Published var textFieldValue1 = ""
#Published var textFieldValue2 = "Second field"
#Published var showTextField2 = false
private var cancellable : AnyCancellable?
init() {
cancellable = $textFieldValue1.sink { value in
withAnimation {
self.showTextField2 = !value.isEmpty
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
TextField("", text: $viewModel.textFieldValue1)
.textFieldStyle(RoundedBorderTextFieldStyle())
if viewModel.showTextField2 {
TextField("", text: $viewModel.textFieldValue2)
.textFieldStyle(RoundedBorderTextFieldStyle())
.transition(.scale)
}
}
}
}
Is that approaching what you're attempting to build ?
struct ContentView: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
#State var editingNewPlayer = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(participants, id: \.self) { participant in
Text(participant)
.padding(.trailing)
Divider()
}
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName, onEditingChanged: { edit in
editingNewPlayer = edit
}, onCommit: handleNewPlayerEntry)
})
if editingNewPlayer {
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName) { edit in
editingNewPlayer = false
}
})
}
}
.padding(.leading)
.frame(maxHeight: .infinity, alignment: .top)
.transition(.opacity)
.animation(.easeIn)
}
func handleNewPlayerEntry() {
if newPersonName.count > 0 {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
editingNewPlayer = false
}
}
}
}

Dismiss picker in SwiftUI and reset picker selection to initial value

I am making a view in SwiftUI with a picker-popover. When picking a value and dismissing the view everything works fine.
But I need to be able to dismiss the picker WITHOUT setting the newly selected value, and have it go back to the initial value it had when being opened.
You can see the code here:
import SwiftUI
struct ContentView: View {
#State var showPicker = false
#State var selectedPickerOption = 0
let pickerOptions = ["Hello", "World", "Yes"]
var body: some View {
ZStack {
VStack {
Text("Selected Option: \(pickerOptions[selectedPickerOption])")
Button(
action: {
showPicker = true
},
label: {
Text("Open Picker")
.padding()
}
)
}
if showPicker {
PickerPopover(
pickerOptions: pickerOptions,
width: 300,
height: 300,
showPicker: $showPicker,
selectedPickerOption: $selectedPickerOption,
initialPickerOption: selectedPickerOption
)
.background(Color.red)
}
}
}
}
struct PickerPopover: View {
var pickerOptions: [String]
var width: CGFloat
var height: CGFloat
#Binding var showPicker: Bool
#Binding var selectedPickerOption: Int
var initialPickerOption: Int // This one doesn't work yet
func selectOption() {
withAnimation {
showPicker.toggle()
}
}
func cancel() {
// ######### THIS LINE HERE ISN'T WORKING ##############
selectedPickerOption = initialPickerOption
withAnimation {
showPicker.toggle()
}
}
var body: some View {
VStack {
Picker(
selection: $selectedPickerOption,
label: Text("")
) {
ForEach(0 ..< pickerOptions.count) {
Text(self.pickerOptions[$0])
}
}
.pickerStyle(WheelPickerStyle())
Button(action: cancel) {
Text("Cancel")
}
Button(action: selectOption) {
Text("Select")
}
}
.transition(.move(edge: .bottom))
}
}
I believe the first line in the cancel() function should do the trick - if selectedPickerOption is set to 0 (or 1, etc) that will reset the picker to that index specifically.
I have been unable to set it dynamically though. I have tried passing in an additional value (intialPickerOption), but resetting selectedPickerOption = initialPickerOption does seem to set it to the actual currently selected selectedPickerOption, and the picker behaves as if that was chosen correctly.
What am I possibly missing here?
The problem occurs as you are modifying selectedPickerOption which will cause your ContentView to reload whenever the picker changes. Hence, you will pass the selected value as initialPickerOption. selectedPickerOption will always be the same like your initial value.
Here is a solution with using local State in your PickerView and then sync the Binding on Select or don't sync it. I comment the code at these parts
struct PickerPopover: View {
var pickerOptions: [String]
var width: CGFloat
var height: CGFloat
#Binding var showPicker: Bool
#Binding var selectedPickerOption: Int
#State var localState : Int = 0 //<< Here your local State
func selectOption() {
self.selectedPickerOption = localState //<< Sync the binding with the local State
withAnimation {
showPicker.toggle()
}
}
func cancel() {
//<< do nothing here
withAnimation {
showPicker.toggle()
}
}
var body: some View {
VStack {
Picker(
selection: $localState,
label: Text("")
) {
ForEach(0 ..< pickerOptions.count) {
Text(self.pickerOptions[$0])
}
}
.pickerStyle(WheelPickerStyle())
Button(action: cancel) {
Text("Cancel")
}
Button(action: selectOption) {
Text("Select")
}
}
.transition(.move(edge: .bottom))
.onAppear {
self.localState = selectedPickerOption // << set inital value here
}
}
}

Navigation Bar Buttons sometimes are not tappable

I have FavoritesView set with a navigationBarItem Button to display SettingsView as a modal sheet. However, both this button, and the Done button in the sheet only register and respond to some taps. I can't see any pattern to when the buttons will respond, but sometimes it can take 4 or 5 taps before the app responds! Any way to fix this behaviour?
FavoritesView:
import SwiftUI
enum ActiveSheet {
case details, settings
}
struct FavoritesView: View {
let speakers: [Speaker] = Bundle.main.decode("SpeakerTestData.json")
#State private var selectedModel: Speaker?
#State private var showingSheet = false
#State private var activeSheet: ActiveSheet = .settings
#EnvironmentObject var favorites: Favorites
#EnvironmentObject var settings: UserSettings
var filteredFavorites: [Speaker] {
let allSpeakers = speakers
var filteredItems: [Speaker] = []
for entry in allSpeakers {
if favorites.contains(entry) {
filteredItems.append(entry)
}
}
let sorted = filteredItems.sorted {
$0.model.localizedStandardCompare($1.model) == .orderedAscending
}
return sorted
}
var body: some View {
NavigationView {
List {
if filteredFavorites.count == 0 {
Text("Items you favourite will appear here.")
.foregroundColor(.secondary).padding(5)
} else {
Section(header: Text("Speakers")) {
ForEach(filteredFavorites) { speaker in
HStack {
Button(action: {
self.activeSheet = .details
self.selectedModel = speaker
self.showingSheet = true
}) {
SpeakerModelRow(speaker: speaker).contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button(action: {
if self.favorites.contains(speaker) {
self.favorites.remove(speaker)
} else {
self.favorites.add(speaker)
}
}, label: {
if self.favorites.contains(speaker) {
Image(systemName: "star.fill")
.foregroundColor(.blue)
.font(Font.title.weight(.ultraLight))
} else {
Image(systemName: "star")
.foregroundColor(.gray)
.font(Font.title.weight(.ultraLight))
}
}
).padding(5)
}
}
}
}
}
.navigationBarTitle("Favourites")
.navigationBarItems(trailing:
Button(action: {
self.activeSheet = .settings
self.showingSheet = true
}){
Image(systemName: "gear").font(Font.title.weight(.ultraLight)).padding(.trailing, 5).foregroundColor(.primary)
})
.sheet(isPresented: self.$showingSheet) {
if self.activeSheet == .details {
SpeakerDetailView(speaker: self.selectedModel!, showSheet: self.$showingSheet).environmentObject(self.favorites).environmentObject(self.settings)
} else {
SettingsView(showSheet: self.$showingSheet).environmentObject(self.settings)
}
}
}
}
}
SettingsView presented as a sheet:
struct SettingsView: View {
#EnvironmentObject var settings: UserSettings
#Binding var showSheet: Bool
#State var result: Result<MFMailComposeResult, Error>? = nil
#State var isShowingMailView = false
var body: some View {
NavigationView {
VStack {
//SettingsView Window
}.navigationBarTitle("Settings")
.navigationBarItems(leading: Button("Done") {
self.showSheet = false
})
}
}
}
It is known issue, try to use internal button content padding (either default or more), like
.navigationBarItems(leading: Button(action: {
self.showSheet = false
}) { Text("Done").padding() }