I'm trying to use the SwiftUI 2 ScrollViewReader with a picker so that tapping on the
picker displays the picker list with the current selection in the view (preferably to an anchor). Nothing that I have tried, including the below, even has any effect on the list. It seems like this should be simple.
struct ContentView: View {
#State private var topIntYears: [Int] = createIntYearList()
#State private var startYear = 1923
#State private var startDollars: String = "1"
var body: some View {
VStack {
Text("Do Something")
NavigationView {
ScrollViewReader { svr in
Form {
Picker("Base Year", selection: $startYear) {
ForEach(topIntYears, id:\.self) {
Text(String($0))
//this does not work
.simultaneousGesture(
TapGesture()
.onEnded { _ in
print("Picker tapped")
svr.scrollTo(startYear)
}
)
}
}//pick
TextField("Base dollars", text: $startDollars)
.keyboardType(.decimalPad)
.padding()
}//form
}//svr
}//nav
}//v
}//body
}
func createIntYearList() -> [Int] {
var years: [Int] = []
for y in 1898...2021 {
years.append(y)
}
return years
}//int years
Xcode 12.4, iOS 14.4 Any guidance would be appreciated.
Related
I'm experiencing a truly bizarre behavior with an app that has pickers in a view that
calls a map as a sheet modal. The pickers are not directly involved in calling the map -
but set conditions for the annotations that will be displayed. And in the simple example
I have included below, it is clear the issue has nothing to do with the map - the
problem behavior is present with a simple text view in the modal.
The issue: if the user swipes to dismiss the modal it appears to always work as expected.
If the user taps a button to dismiss the modal with either the environment dismiss or
through a binding to the #State that calls the view, then after the second showing of the
modal, you can no longer raise the picker - the displayed value just turns color - as if
a color toggle on tap. I've also tried showing the modal as full screen, as an animation
and as a transition. I get the same result every time. I'm at the point where I think this
must be an Xcode bug, but I hope someone can show me a solution. The same behavior exists
in Preview, Simulator and a real Device.
Here is a very stripped down example which demonstrates the issue:
enum StorageKeys: String {
case appChosenFuel, appChosenState
}//enum
struct ContentView: View {
#State private var showGroupMapView: Bool = false
#State private var showTooBigAlert: Bool = false
#State private var justANumber: Int = 500
var body: some View {
NavigationView {
VStack {
Text("Picker plus Sheet Test")
.padding()
FuelPickerView()
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
if justANumber > 1000 {
showTooBigAlert = true
} else {
withAnimation {
showGroupMapView.toggle()
}
}
} label: {
Image(systemName: "map")
.font(.system(size: 20))
.frame(width: 40, height: 40)
}
.sheet(isPresented: $showGroupMapView, onDismiss: {
print("you dismissed me")
}) {
GroupMapView()
}
}//toolbar group
}//toolbar
}//nav
}//body
}//sruct
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct GroupMapView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Text("This is the map view")
VStack {
HStack {
Spacer()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "gear")
.padding(.trailing, 20)
.padding(.top, 20)
}
}
Spacer()
}
}//z
}
}//struct
class FuelPickerViewModel: ObservableObject {
struct FuelItem: Identifiable, Hashable {
let id = UUID()
let name: String
let initials: String
}
#Published var fuelItems = [
FuelItem(name: "Biodiesel (B20 and above)", initials: "BD"),
FuelItem(name: "Compressed Natural Gas", initials: "CNG"),
FuelItem(name: "Ethanol (E85)", initials: "E85"),
FuelItem(name: "Electric", initials: "ELEC"),
FuelItem(name: "Hydrogen", initials: "HY"),
FuelItem(name: "Liquified Natural Gas", initials: "LNG"),
FuelItem(name: "Liquified Petroleum Gas (Propane)", initials: "LPG")
]
}//class
struct FuelPickerView: View {
#AppStorage(StorageKeys.appChosenFuel.rawValue) var appChosenFuel = "ELEC"
#AppStorage(StorageKeys.appChosenState.rawValue) var appChosenState = "CO"
#StateObject var fuelPickerVM = FuelPickerViewModel()
var body: some View {
return VStack {
Picker("Fuel", selection: $appChosenFuel) {
ForEach(fuelPickerVM.fuelItems, id: \.self) {
Text($0.initials)
}
}
}
}//body
}//struct
And after the second modal display/dismiss with the button, tapping the picker does
nothing except change the background color:
Any guidance would be appreciated: Xcode 13.2.1 iOS 15.2
I have an app that has a datepicker (GraphicalDatePickerStyle). It hidden by default. When you tap the view that contains it, it becomes visible.
DateTimePicker
struct DateTimePicker<Content: View>: View {
#Binding var selection: Date
#State private var isDatePickerVisible: Bool = false
private var displayedComponents: DatePickerComponents
private var content: (_ isVisible: Bool) -> Content
init(selection: Binding<Date>,
displayedComponents: DatePickerComponents = [.date],
content: #escaping (_ isVisible: Bool) -> Content) {
self._selection = selection
self.content = content
self.displayedComponents = displayedComponents
}
var body: some View {
VStack(alignment: .center) {
self.content(isDatePickerVisible)
.onTapGesture {
withAnimation {
self.isDatePickerVisible.toggle()
}
}
if isDatePickerVisible {
VStack {
DatePicker("",
selection: $selection,
displayedComponents: self.displayedComponents)
.labelsHidden()
.datePickerStyle(GraphicalDatePickerStyle())
}
}
}
}
}
ContentView that contains DateTimePicker
struct ContentView: View {
#State var selection = Date()
var body: some View {
ScrollView(.vertical) {
DateTimePicker(selection: $selection) { _ in
HStack {
Text("Date ")
Spacer()
Text("\(selection)")
}
}
}
}
}
There are a few memory leaks if you show and hide DateTimePicker. I tried a lot but could not fix it.
I have a navigation requirement that looks something like this:
Each detail screen can navigation to the next and previous detail screen. At the same time, the "back" button should always go back to the main list (not the previous detail screen).
I'm struggling with how to accomplish this in SwiftUI?
Here is what I have so far:
struct ListView: View {
#State private var currentDetailShown: Int?
#State private var listItems: [Int] = Array(repeating: 0, count: 10)
func goToNext() {
if let idx = self.currentDetailShown {
self.currentDetailShown = min(self.listItems.count - 1, idx + 1)
}
}
func goToPrev() {
if let idx = self.currentDetailShown {
self.currentDetailShown = max(0, idx - 1)
}
}
var body: some View {
List {
ForEach(0..<listItems.count) { index in
NavigationLink(destination: DetailView(goToNext: self.goToNext, goToPrev: self.goToPrev),
tag: index,
selection: self.$currentDetailShown) {
ListItem(score: listItems[index])
}
.isDetailLink(false)
.onTapGesture {
self.currentDetailShown = index
}
}
}
}
}
What happens with this code is that from the first detail view, it'll move to the to the next detail view and then immediately jump back to the list view.
I feel like I'm overthinking this or missing something obvious...
Instead of navigating to each detail from your list, you can navigate to a detailView that can show each detail individually by using a published variable in an observable object. Here is an example
struct MainView: View{
#EnvironmentObject var viewModel: ViewModel
var body: some View{
NavigationView{
VStack{
ForEach(self.viewModel.details, id:\.self){ detail in
NavigationLink(destination: DetailView(detail: self.viewModel.details.firstIndex(of: detail)!).environmentObject(ViewModel())){
Text(detail)
}
}
}
}
}
}
class ViewModel: ObservableObject{
#Published var showingView = 0
#Published var details = ["detail1", "detail2", "detail3", "detail4", "detail5", "detail6"]
}
struct DetailView: View{
#EnvironmentObject var viewModel: ViewModel
#State var detail: Int
var body: some View{
VStack{
IndivisualDetailView(title: viewModel.details[detail])
Button(action: {
self.viewModel.showingView -= 1
}, label: {
Image(systemName: "chevron.left")
})
Button(action: {
self.viewModel.showingView += 1
print(self.viewModel.showingView)
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct IndivisualDetailView: View{
#State var title: String
var body: some View{
Text(title)
}
}
I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.
I'm trying to reusable View and I added it on ContentView
This is my Child View
struct VStackView: View {
#Binding var spacing: Double
#Binding var alignmentIndex: Int
#Binding var elementsCount: Int
private let alignments: [HorizontalAlignment] = [.leading, .center, .trailing]
var body: some View {
VStack(alignment: self.alignments[alignmentIndex], spacing: CGFloat(spacing)) {
ForEach(0..<elementsCount) {
Text("\($0)th View")
}
}
}
}
and This is SuperView
Superview has Controls like Stepper, Slider, Picker that adjust values of VStack (alignment, spacing etc)
and I want to show the result depending on that values. but Child View is not changed
struct LayoutView: View {
private let layout: StackLayout
#State private var spacing = 0.0
#State private var alignmentIndex = 0
#State private var alignment: HorizontalAlignment = .leading
#State private var elementsCount: Int = 0
private let alignmentsString = [".leading", ".center", ".trailing"]
private let minValue = 0.0
private let maxValue = 100.0
init(_ layout: StackLayout) {
self.layout = layout
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Controls")) {
VStack(alignment: .leading) {
Text("Spacing: \(Int(spacing))").font(.caption)
HStack {
Text("\(Int(minValue))")
Slider(value: $spacing, in: minValue...maxValue, step: 1)
Text("\(Int(maxValue))")
}
Divider()
Picker("alignment", selection: $alignmentIndex) {
ForEach(0..<self.alignmentsString.count) {
Text("\(self.alignmentsString[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
Divider()
Stepper(value: $elementsCount, in: 0...10) {
Text("Element Count: \(elementsCount)")
}
}
}
VStackView(spacing: $spacing, alignmentIndex: $alignmentIndex, elementsCount: $elementsCount)
}
.navigationBarTitle(Text(layout.rawValue), displayMode: .inline)
}
}
}
I also search google and they recommend #EnviornmentObject. if that is correct, when to use #Binding property wrapper.
Isn't it two way binding properties?
Simply speaking you can use #Binding, when you want to share data in two places.
#Observable or #environmetobject is to be used, when you want to share your data in multiple views.
Your ForEach Loop in the VStackView generates a problem, because Swiftui does not know how it can identify each of your items uniquely so it does not know how to update them, when values change.
Append your code like this:
ForEach(0..<elementsCount, id: \.self) {
Text("\($0)th View")
}