Displaying a Sheet from multiple options in swiftUI - ios

Follow-up question to iOS14 introducing errors with #State bindings
I was displaying a modal sheet from several options, depending on which button was pressed. However, now in iOS14 I get a fatal error caused by the selectedSpeaker/selectedMicrophone/selectedAmp being nil when the sheet displays. I am trying to change to .sheet(item:, content:) but I can't see how to implement the enum and then pass in the appropriate selected object. This is what I was doing previously:
enum ActiveSheet {
case speakerDetails, micDetails, ampDetails, settings
}
struct FavoritesView: View {
#State private var selectedSpeaker: Speaker?
#State private var selectedMicrophone: Microphone?
#State private var selectedAmp: Amplifier?
#State private var showingSheet = false
#State private var activeSheet: ActiveSheet = .settings
var body: some View {
List {
Button(action: {
self.activeSheet = .settings
self.showingSheet = true
}, label: { Text("Settings")})
Button(action: {
self.activeSheet = .micDetails
self.selectedMicrophone = microphones[0]
self.showingSheet = true
}, label: { Text("Mic 1")})
Button(action: {
self.activeSheet = .micDetails
self.selectedMicrophone = microphones[1]
self.showingSheet = true
}, label: { Text("Mic 2")})
Button(action: {
self.activeSheet = .speakerDetails
self.showingSheet = true
self.selectedSpeaker = speakers[0]
}, label: { Text("Speaker 1")})
Button(action: {
self.activeSheet = .speakerDetails
self.showingSheet = true
self.selectedSpeaker = speakers[1]
}, label: { Text("Speaker 2")})
//and so on for activeSheet = .ampDetails in the same way.
}
.sheet(isPresented: self.$showingSheet) {
if self.activeSheet == .speakerDetails {
SpeakerDetailView(speaker: self.selectedSpeaker!)
}
else if self.activeSheet == .micDetails {
MicDetailView(microphone: self.selectedMicrophone!)
}
else if self.activeSheet == .ampDetails {
AmpDetailView(amp: self.selectedAmp!)
} else if self.activeSheet == .settings {
SettingsView(showSheet: self.$showingSheet))
}
}
}
}
}

Here is another approach for your problem which uses sheet(item:content:)
struct ContentView: View {
#State private var selectedSpeaker: Speaker?
#State private var selectedMicrophone: Microphone?
#State private var selectedAmp: Amplifier?
#State private var showSettingsSheet = false
var body: some View {
List {
settingsSection
microphonesSection
// more sections
}
}
var settingsSection: some View {
Button(action: {
self.showSettingsSheet = true
}) {
Text("Settings")
}
.sheet(isPresented: self.$showSettingsSheet) {
SettingsView()
}
}
#ViewBuilder
var microphonesSection: some View {
Button(action: {
self.selectedMicrophone = microphones[0]
}) {
Text("Mic 1")
}
Button(action: {
self.selectedMicrophone = microphones[1]
}) {
Text("Mic 2")
}
.sheet(item: self.$selectedMicrophone) {
MicDetailView(microphone: $0)
}
}
}
This way you also don't need enum ActiveSheet.
You can always use #Environment(\.presentationMode) to dismiss a sheet, no need to pass the variable to the sheet (as in SettingsView(showSheet: self.$showingSheet)):
struct SettingsView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Text("SettingsView")
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}

Related

Creating an iOS passcode view with SwiftUI, how to hide a TextView?

I am trying to imitate a lock screen of iOS in my own way with some basic code. However I do not understand how to properly hide an input textview. Now I am using an opacity modifier, but it does not seem to be the right solution. Could you please recommend me better options?
import SwiftUI
public struct PasscodeView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var viewModel: ContentView.ViewModel
private let maxDigits: Int = 4
private let userPasscode = "1234"
#State var enteredPasscode: String = ""
#FocusState var keyboardFocused: Bool
#State private var showAlert = false
#State private var alertMessage = "Passcode is wrong, try again!"
public var body: some View {
VStack {
HStack {
ForEach(0 ..< maxDigits) {
($0 + 1) > enteredPasscode.count ?
Image(systemName: "circle") :
Image(systemName: "circle.fill")
}
}
.alert("Wrong Passcode", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
}
TextField("Enter your passcode", text: $enteredPasscode)
.opacity(0)
.keyboardType(.decimalPad)
.focused($keyboardFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
keyboardFocused = true
}
}
}
.padding()
.onChange(of: enteredPasscode) { _ in
guard enteredPasscode.count == maxDigits else { return }
passcodeValidation()
}
}
func passcodeValidation() {
if enteredPasscode == userPasscode {
viewModel.isUnlocked = true
dismiss()
} else {
enteredPasscode = ""
showAlert = true
}
}
}

How to select only one checkbox in foreach in SwiftUI?

I am trying to show checkbox in foreach loop but when i click on any one of them all are selected.
How can we separate them.
struct Screen: View {
#State private var checked = true
var data = ["1","2", "3"]
var body: some View {
ForEach( data.indices, id:\.self ) { item in
HStack {
CheckBoxView(checked: $checked)
Text(data[item])
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
var body: some View {
Image(systemName: checked ? "checkmark.square.fill" : "square")
.foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
self.checked.toggle()
}
}
}
Thank You for help.
The easiest way is to use an array of state variables:
struct Screen: View {
#State private var checked: [Bool] = [true, true, true]
var data = ["1","2", "3"]
var body: some View {
VStack {
ForEach( data.indices, id:\.self ) { index in
HStack {
CheckBoxView(checked: $checked[index])
Text(data[index])
}
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
var body: some View {
Image(systemName: checked ? "checkmark.square.fill" : "square")
.foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
self.checked.toggle()
}
}
}
}
However, I personally do not like this solution, because the state array is not dynamic in size to your data. With this initialization your state array is always the same size as your data.
struct Screen: View {
var data = ["1","2", "3"]
#State private var checked: [Bool]
init() {
_checked = State(initialValue: [Bool](repeating: false, count: data.count))
}
var body: some View {
VStack {
ForEach( data.indices, id:\.self ) { index in
HStack {
CheckBoxView(checked: $checked[index])
Text(data[index])
}
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
var body: some View {
Image(systemName: checked ? "checkmark.square.fill" : "square")
.foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
self.checked.toggle()
}
}
}
}
Here is an answer that will allow you to check one or more boxes, and keep track of which values were selected. It uses an .onChanged to keep track of the actual value that has been selected as the check box itself is just UI:
struct Screen: View {
var data = ["1","2","3"]
#State var selectedItems: Set<String> = [] // Use a Set to keep track of multiple check boxes
#State var selectedItems = "" // Use a String to keep track of only one.
var body: some View {
List {
ForEach( data, id:\.self ) { item in
CheckBoxRow(title:item, selectedItems: $selectedItems, isSelected: selectedItems.contains(item))
}
}
}
}
struct CheckBoxRow: View {
var title: String
#Binding var selectedItems: Set<String>
#State var isSelected: Bool
var body: some View {
GeometryReader { geometry in
HStack {
CheckBoxView(checked: $isSelected, title: title)
.onChange(of: isSelected) { _ in
if isSelected {
selectedItems.insert(title)// or
selectedItems = title
} else {
selectedItems.remove(title)// or
selectedItems = ""
}
}
}
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
#State var title: String
var body: some View {
HStack {
Image(systemName: (checked ? "checkmark.square" : "square"))
Text(title)
.padding(.leading)
}
.onTapGesture {
self.checked.toggle()
}
}
}

Displaying multiple overlays with SwiftUI

I'm trying to show multiple sheets with SwiftUI.
Overlay.swift
import SwiftUI
struct OverlayWith<Content: View>: View {
let content: () -> Content
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.compact.down")
.resizable()
.frame(width: 40, height: 15)
.accentColor(Color.gray)
.padding()
}
content()
Spacer()
}
}
}
struct OverlayView_Button1: View {
var body: some View{
Text("Button 1 triggered this overlay")
}
}
struct OverlayView_Button2: View {
var body: some View{
Text("Button 2 triggered this overlay")
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var overlayVisible: Bool = false
// Button States
#State var view1_visible: Bool = false
#State var view2_visible: Bool = false
var body: some View {
VStack() {
Button(action: {
self.showSheet1()
}, label: {
Text("Button 1")
})
Button(action: {
self.showSheet2()
}, label: {
Text("Button 2")
})
}
.sheet(isPresented: $overlayVisible, content:{
if self.view1_visible {
OverlayWith(content: {
OverlayView_Button1()
})
} else if self.view2_visible {
OverlayWith(content: {
OverlayView_Button2()
})
}
})
}
func showSheet1(){
self.resetButtonStates()
self.view1_visible = true
self.overlayVisible = true
}
func showSheet2(){
self.resetButtonStates()
self.view2_visible = true
self.overlayVisible = true
}
func resetButtonStates(){
self.view1_visible = false
self.view2_visible = false
}
}
Compiling this code for iOS 13 works as expected. For iOS 14 on the other hand showSheetX() opens a sheet with an empty view. Dismissing the sheet and opening it again shows OverlayView_ButtonX. Debugging shows that viewX_visible is false when showSheetX() was called for the first time.
Is this a bug with iOS itself or am I missing something here?
Thank you in advance!
I think I found the solution for this issue:
ContentView.swift
import SwiftUI
var view1_visible: Bool = false // <- view1_visible is not a state anymore
var view2_visible: Bool = false // <- view2_visible is not a state anymore
struct ContentView: View {
#State var overlayVisible: Bool = false
var body: some View {
VStack() {
Button(action: {
self.showSheet1()
}, label: {
Text("Button 1")
})
Button(action: {
self.showSheet2()
}, label: {
Text("Button 2")
})
}
.sheet(isPresented: $overlayVisible, content:{
if view1_visible {
OverlayWith(content: {
OverlayView_Button1()
})
} else if view2_visible {
OverlayWith(content: {
OverlayView_Button2()
})
}
})
}
func showSheet1(){
self.resetButtonStates()
view1_visible = true
self.overlayVisible = true
}
func showSheet2(){
self.resetButtonStates()
view2_visible = true
self.overlayVisible = true
}
func resetButtonStates(){
view1_visible = false
view2_visible = false
}
}
Feel free to explain to me why this code works. I'm really confused :S

SwiftUI: Using #Binding to dismiss a modal view not working

I'm passing a #State var down a few views, using #Binding on the child views and when I ultimately set the variable to back to false, sometimes my view doesn't dismiss.
It seems like I can run articleDisplayed.toggle() but if I run an additional function above or below, it won't work.
Any idea what's going on here?
Here's my code:
struct HomeView: View {
#EnvironmentObject var state: AppState
#State var articleDisplayed = false
// MARK: - Body
var body: some View {
NavigationView {
ZStack {
List {
ForEach(state.cards, id: \.id) { card in
Button(action: {
self.articleDisplayed = true // I set it to true here
self.state.activeCard = card
}) {
HomeCell(
card: card,
publicationColor: self.state.publication.brandColor
)
}.sheet(isPresented: self.$articleDisplayed) {
SafariQuickTopicView(articleDisplayed: self.$articleDisplayed)
.environmentObject(self.state)
.environment(\.colorScheme, .light)
}
}
}
}
}
}
}
Then in my SafariQuickTopicView:
struct SafariQuickTopicView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var state: AppState
#Binding var articleDisplayed: Bool
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
// doesn't matter what's in here
}
.navigationBarItems(trailing: passButton)
}
}
private var passButton: some View {
Button(action: self.state.pass {
DispatchQueue.main.async {
// self.state.removeActiveCardFromState()
self.articleDisplayed.toggle() // this will work but adding a second function in here prevents it from working, above or below the toggle.
}
}
}) {
Text("Pass")
}
}
Finally, in my AppState:
func pass(completion: () -> Void) { // need completion?
guard let activeCard = activeCard else { return }
if let index = cards.firstIndex(where: { $0.id == activeCard.id }) {
activeCard.add(comment: "pass")
rejectCurrentCard() // Does an async operation with an external API but we don't care about the result
addRemovedActiveCardToUserDefaults()
completion()
}
}
Move .sheet out of List, it must be one per view hierarchy, so like
List {
ForEach(state.cards, id: \.id) { card in
Button(action: {
self.articleDisplayed = true // I set it to true here
self.state.activeCard = card
}) {
HomeCell(
card: card,
publicationColor: self.state.publication.brandColor
)
}
}
}
.sheet(isPresented: self.$articleDisplayed) {
SafariQuickTopicView(articleDisplayed: self.$articleDisplayed)
.environmentObject(self.state)
.environment(\.colorScheme, .light)
}

How to implement function like onAppear in SwiftUI?

So, I would like to create a custom view and add function. How can I implement a function like .onAppear(perform: (() -> Void)?)? My code does not work, onDismiss closure does not call in the DashboardView.
struct DashboardView: View {
#State var employees = ["Alex", "Olga", "Mark"]
#State var presentEmployeeView = false
var body: some View {
NavigationView {
List {
Section {
Button(action: {
self.presentEmployeeView = true
}, label: {
Text("All employees")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
.sheet(isPresented: $presentEmployeeView) {
EmployeesView(employees: self.employees).onDismiss {
self.presentEmployeeView = false
}
}
}
}
struct EmployeesView: View {
let employees: [String]
#State private var onDismissClosure: (() -> Void)? = nil
func onDismiss(perform action: (() -> Void)? = nil) -> some View {
self.onDismissClosure = action
return self
}
var body: some View {
NavigationView {
List {
ForEach(employees) { employee in
EmployeeCell(employee: employee)
}
}.navigationBarItems(leading:
Button(action: {
self.onDismissClosure?()
}, label: {
Text("Close")
})
)
}
}
}
Here is possible approach. Tested & worked with Xcode 11.4 / iOS 13.4
struct DashboardView: View {
#State var employees = ["Alex", "Olga", "Mark"]
#State var presentEmployeeView = false
var body: some View {
NavigationView {
List {
Section {
Button(action: {
self.presentEmployeeView = true
}, label: {
Text("All employees")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
.sheet(isPresented: $presentEmployeeView) {
EmployeesView(employees: self.employees) {
self.presentEmployeeView = false
}
}
}
}
struct EmployeesView: View {
let employees: [String]
var onDismiss = {}
var body: some View {
NavigationView {
List {
ForEach(employees, id: \.self) { employee in
Text("\(employee)")
}
}.navigationBarItems(leading:
Button(action: {
self.onDismiss()
}, label: {
Text("Close")
})
)
}
}
}
Update: possible alternate for usage with modifier:
...
.sheet(isPresented: $presentEmployeeView) {
EmployeesView(employees: self.employees).onDismiss {
self.presentEmployeeView = false
}
}
}
}
struct EmployeesView: View {
let employees: [String]
var onDismiss = {}
func onDismiss(_ callback: #escaping () -> ()) -> some View {
EmployeesView(employees: employees, onDismiss: callback)
}
var body: some View {
NavigationView {
List {
ForEach(employees, id: \.self) { employee in
Text("\(employee)")
}
}.navigationBarItems(leading:
Button(action: {
self.onDismiss()
}, label: {
Text("Close")
})
)
}
}
}

Resources