in SwifUI how to define variables which depends of an input Entity in the init of a view and then populate different pickers - swiftui

I have a CoreData entity called PokSession which contains multiple attributes (date, currency, period, nbheure).
I want to design a view which display those informations (and allow me to modify them) for a given entity.
So basically from a previous view, I call an other view like this:
#FetchRequest(entity: PokSession.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \PokSession.date, ascending: false)
]) var poksessions: FetchedResults<PokSession>
ForEach(poksessions, id: \.date) { session in
DetailSessionPokUIView (session: session)
}
.onDelete(perform: deleteSessions)
which leads to the following view:
struct DetailSessionPokUIView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var session: PokSession
#State private var date: Date
#State private var currency: String
#State private var periode: String
#State private var nbheure: Double
let liste_currency = ["CAD", "EUR", "USD", "GBP"]
let liste_periode = ["matinée", "après-midi", "soirée"]
init(session: PokSession) {
date = session.date!
currency = session.currency!
periode = session.periode!
nbheure = session.nbheure
}
var body: some View {
NavigationView {
Form {
Section {
DatePicker(selection: $date , displayedComponents: .date){
Text("Date")
}
HStack{
Picker("Devise", selection: $currency) {
ForEach(liste_currency, id: \.self) { currency in
Text(currency)
}
}
}
HStack{
Picker("Période de jeu", selection: $periode) {
ForEach(liste_periode, id: \.self) { periode in
Text(periode)
}
}
}
HStack{
Text("Temps de jeu (h)")
.multilineTextAlignment(.leading)
Slider(value: $nbheure, in: 0...24, step: 1)
Text("\(nbheure, specifier: "%.0f")")
Image(systemName: "clock")
}
}
} // Form
} // NavigationView
}
}
But I am having error message inside my init(), saying " Variable 'self.session' used before being initialized".
I dont really understand why as "session" is an input in my init().
How can I use attributes of the selected PokSection entity to populate my DatePicker and my other Pickers default value
It shouldn't be difficult I guess but I am struggling...
Basically, I just want to have my Pickers set with the value coming from the selected PokSession.
And I want to see it in a Picker because I want to be able to modify it.
Thanks for your help

Related

How to select an item by a button or tapGesture in my SwiftUI List and transfer it to another view

In my app I´m needing two players (only 2 names/strings), selected from an array built in a List/ForEach SwiftUI-code, which are used in another view.
What is the way to bring the name into a string for my Text(item)?
Can I select two items out of the list?
Thx for any help.
Franz
My code (modified, found by Ale Patron,Tutorial using UserDefaults with encoding and decoding the array/list ):
#State private var allTeams: [PlayerItem] = []
#State private var newPlayer = ""
#State private var selectedPlayer = ""
#State private var selection: String?
struct PlayerItem: Identifiable {
var id = UUID()
let player: String
}
var body: some View {
VStack{
HStack {
TextField("Add Players/Teams...", text: $newPlayer)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.allTeams.append(PlayerItem(player: self.newPlayer))
self.newPlayer = ""
}) {
Image(systemName: "plus")
}
.padding(.leading, 5)
}.padding()
List{
ForEach(allTeams) { playerItem in
Text(playerItem.player)
}
.onTapGesture {
print("How can I select my first und my second player")
}
}
Text("Selected Player: \(selectedPlayer)")
}
}
}
You should use indices for what you are trying to do.
Try this:
ForEach(allTeams.indices) { i in
Text(allTeams[i].player)
.onTapGesture {
print("How can I select my first und my second player")
print("The selected player is \(allTeams[i].player). The second player is \(allTeams[i + 1].player)"
}
}
Make sure to also check if the selected player is the last one in the array and to catch this. Otherwise, you may encounter an out-of-bounds error.

How Share Variables Among View and Sub Views in SwiftUI

I have a date picker, when the date selection value changes, I would like to store it in a string array called filterSelections, how do I do that? Thanks in advance.
import SwiftUI
public var filterSelections: [String: Any]?
func setFilterSelections(name: String, selectedValue: Any) {
filterSelections[name] = selectedValue
}
struct myMainSwiftUIView: View{
var body: some View {
ScrollView {
VStack{
mySub1View()
}
}
}
}
struct mySub1View: View {
#State public var fromDate: Date = Calendar.current.date(byAdding: DateComponents(year: -40), to: Date()) ?? Date()
var body: some View {
HStack(spacing:10) {
VStack(alignment:.leading, spacing:20) {
DatePicker(selection: $fromDate, displayedComponents: .date) {
Text("From")
.font(.body)
.fixedSize()
}
}
}
}
}
}
It's hard for me to see the application of what storing all of the changes to the date picker would be, since there wouldn't be any way to cancel them out (and, in pre-iOS 14, I think the wheel would make this a particular crazy looking list when things were changing).
My suspicion is that you probably want the date along with some other filters added together. And, you specified wanting to share that state between views and subviews, which I've tried to accommodate. I also used the date format that you asked for.
I did not include the [String:Any] as your question said "array", not dictionary.
Lots of guess work here, since it's not totally clear what your goal is, but hopefully this gives you some ideas of how to share state.
class FilterViewModel : ObservableObject {
#Published var dateFilter : Date = Calendar.current.date(byAdding: DateComponents(year: -40), to: Date()) ?? Date()
#Published var myOtherFilter = "Filter1"
static var formatter = DateFormatter()
var allFilters : [String] {
Self.formatter.dateFormat = "yyyy/MM/dd"
return [myOtherFilter, Self.formatter.string(from: dateFilter)]
}
}
struct ContentView: View{
#StateObject private var filterModel = FilterViewModel()
var body: some View {
ScrollView {
VStack{
MySub1View(filterModel: filterModel)
}
ForEach(filterModel.allFilters, id: \.self) { filter in
Text(filter)
}
}
}
}
struct MySub1View: View {
#ObservedObject var filterModel : FilterViewModel
var body: some View {
HStack(spacing:10) {
VStack(alignment:.leading, spacing:20) {
DatePicker(selection: $filterModel.dateFilter, displayedComponents: .date) {
Text("From")
.font(.body)
.fixedSize()
}
}
}
}
}
It is so simple, make an array and store all of them, do not make more complex in your code, if you want export your Date array then use StateObject, there is really not a big issue. after all then start working on your stored array, for example where and how you want use it!
import SwiftUI
struct ContentView: View {
var body: some View {
mySub1View()
}
}
struct mySub1View: View {
#State private var selection: Date = Date()
#State private var selectionArray: [Date] = [Date]()
var body: some View {
if #available(iOS 14.0, *) {
DatePicker(selection.description, selection: $selection, displayedComponents: .date)
.onChange(of: selection) { newValue in
selectionArray.append(newValue)
print(selectionArray)
}
}
}
}

In SwiftUI List View refresh triggered whenever underlying datasource of list is updated from a view far away in hierarchy

I am trying to write a "Single View App" in SwiftUI. The main design is very simple. I have a list of items (say Expense) which I am displaying in main view in NavigationView -> List.
List View Source Code
import SwiftUI
struct AmountBasedModifier : ViewModifier{
var amount: Int
func body(content: Content) -> some View {
if amount <= 10{
return content.foregroundColor(Color.green)
}
else if amount <= 100{
return content.foregroundColor(Color.blue)
}
else {
return content.foregroundColor(Color.red)
}
}
}
extension View {
func amountBasedStyle(amount: Int) -> some View {
self.modifier(AmountBasedModifier(amount: amount))
}
}
struct ExpenseItem: Identifiable, Codable {
var id = UUID()
var name: String
var type: String
var amount: Int
static var Empty: ExpenseItem{
return ExpenseItem(name: "", type: "", amount: 0)
}
}
class Expenses: ObservableObject {
#Published var items = [ExpenseItem](){
didSet{
let encoder = JSONEncoder()
if let data = try? encoder.encode(items){
UserDefaults.standard.set(data, forKey: "items")
}
}
}
init() {
let decoder = JSONDecoder()
if let data = UserDefaults.standard.data(forKey: "items"){
if let items = try? decoder.decode([ExpenseItem].self, from: data){
self.items = items
return
}
}
items = []
}
}
struct ContentView: View {
#ObservedObject var expenses = Expenses()
#State private var isShowingAddNewItemView = false
var body: some View {
NavigationView{
List{
ForEach(self.expenses.items) { item in
NavigationLink(destination: ExpenseItemHost(item: item, expenses: self.expenses)){
HStack{
VStack(alignment: .leading){
Text(item.name)
.font(.headline)
Text(item.type)
.font(.subheadline)
}
Spacer()
Text("$\(item.amount)")
.amountBasedStyle(amount: item.amount)
}
}
}.onDelete(perform: removeItems)
}
.navigationBarTitle("iExpense")
.navigationBarItems(leading: EditButton(), trailing: Button(action:
{
self.isShowingAddNewItemView.toggle()
}, label: {
Image(systemName: "plus")
}))
.sheet(isPresented: $isShowingAddNewItemView) {
AddNewExpense(expenses: self.expenses)
}
}
}
func removeItems(at offsets: IndexSet){
self.expenses.items.remove(atOffsets: offsets)
}
}
Each row item is NavigationLink that opens the Expense in readonly mode showing all the attributes of Expense Item.
There is an Add button at the top right to let user add a new expense item in list. The AddNewExpenseView (shown as sheet) has access to the list data source. So whenever user adds an new expense then data source of list is updated (by appending new item) and the sheet is dismissed.
Add View Source Code
struct AddNewExpense: View {
#ObservedObject var expenses: Expenses
#Environment(\.presentationMode) var presentationMode
#State private var name = ""
#State private var type = "Personal"
#State private var amount = ""
#State private var isShowingAlert = false
static private let expenseTypes = ["Personal", "Business"]
var body: some View {
NavigationView{
Form{
TextField("Name", text: $name)
Picker("Expense Type", selection: $type) {
ForEach(Self.expenseTypes, id: \.self) {
Text($0)
}
}
TextField("Amount", text: $amount)
}.navigationBarTitle("Add New Expense", displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
if let amount = Int(self.amount){
let expenseItem = ExpenseItem(name: self.name, type: self.type, amount: amount)
self.expenses.items.append(expenseItem)
self.presentationMode.wrappedValue.dismiss()
}else{
self.isShowingAlert.toggle()
}
}, label: {
Text("Save")
}))
.alert(isPresented: $isShowingAlert) {
Alert.init(title: Text("Invalid Amount"), message: Text("The amount should only be numbers and without decimals"), dismissButton: .default(Text("OK")))
}
}
}
}
Expense Detail (Read Only) View Source Code
struct ExpenseItemView: View {
var item: ExpenseItem
var body: some View {
List{
Section{
Text("Name")
.font(.headline)
Text(item.name)
}
Section{
Text("Expense Type")
.font(.headline)
Text(item.type)
}
Section{
Text("Amount")
.font(.headline)
Text("$\(item.amount)")
}
}.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Expense Details"), displayMode: .inline)
}
}
So far everything good. I then thought of adding an Edit button on the ExpenseItem View screen so that user can edit the Expense. I created an edit View which is launched as a sheet from ReadOnly View when Edit button is clicked.
Edit View Code
struct ExpenseItemHost: View {
#State var isShowingEditSheet = false
#State var item: ExpenseItem
#State var itemUnderEdit = ExpenseItem.Empty
var expenses: Expenses
var body: some View {
VStack{
ExpenseItemView(item: self.item)
}
.navigationBarItems(trailing: Button("Edit")
{
self.isShowingEditSheet.toggle()
})
.sheet(isPresented: $isShowingEditSheet) {
EditExpenseItemView(item: self.$itemUnderEdit)
.onAppear(){
self.itemUnderEdit = self.item
}
.onDisappear(){
//TO DO: Handle the logic where save is done when user has explicitly pressed "Done" button. `//Presently it is saving even if Cancel button is clicked`
if let indexAt = self.expenses.items.firstIndex( where: { listItem in
return self.item.id == listItem.id
}){
self.expenses.items.remove(at: indexAt)
}
self.item = self.itemUnderEdit
self.expenses.items.append(self.item)
}
}
}
}
struct EditExpenseItemView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var item: ExpenseItem
static private let expenseTypes = ["Personal", "Business"]
var body: some View {
NavigationView{
Form{
TextField("Name", text: self.$item.name)
Picker("Expense Type", selection: self.$item.type) {
ForEach(Self.expenseTypes, id: \.self) {
Text($0)
}
}
TextField("Amount", value: self.$item.amount, formatter: NumberFormatter())
}
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: Button("Cancel"){
self.presentationMode.wrappedValue.dismiss()
}, trailing: Button("Done"){
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
Screenshots
Problem
I expect that when user is done with editing by pressing Done button the Sheet should come back to ReadOnly screen as this is where user clicked Edit button. But since I am modifying the data source of ListView when Done button is clicked so the ListView is getting recreated/refreshed. So instead of EditView sheet returning to ReadOnly view, the ListView is getting displayed when Done button is clicked.
Since my code is changing the data source of a view which is right now not accessible to user so below exception is also getting generated
2020-08-02 19:30:11.561793+0530 iExpense[91373:6737004] [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7f9a8b021800; baseClass = UITableView; frame = (0 0; 414 896); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x6000010a1110>; layer = <CALayer: 0x600001e8c0e0>; contentOffset: {0, -140}; contentSize: {414, 220}; adjustedContentInset: {140, 0, 34, 0}; dataSource: <_TtGC7SwiftUIP13$7fff2c9a5ad419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x7f9a8a5073f0>>
I can understand why ListView refresh is getting triggered but what I could not figure out is the correct pattern to edit the model as well as not cause the ListView refresh to trigger when we have intermediate screen in between i.e. List View -> ReadOnly -> Edit View.
What is the suggestion to handle this case?

SwiftUI selection in lists not working on reused cells

Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}

Fetching and setting date from datepicker, but still getting old default value

I want to fetch and set date from DatePicker, but my date is not updating. SwiftUI is new to me and I am confused with what type of property wrapper to use. Please help in this and advice when and where to use #State, #Binding, #Published I read some articles but still concept is not clear to me.
Here I used MVVM and SwiftUI and my code as follows.
class MyViewModel:ObservableObject {
#Published var selectedDate : Date = Date()
#Published var selectedDateStr : String = Date().convertDateToString(date: Date())
}
struct DatePickerView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var viewModel : MyViewModel
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
#State private var selectedDate = Date()
var body: some View {
VStack {
//Title
HStack{
Text("SELECT A DATE")
.foregroundColor(.white)
.font(.system(size: 20))
}
.frame(width:UIScreen.main.bounds.width,height: 60)
.background(Color.red)
//Date Picker
DatePicker(selection: $selectedDate, in: Date()-15...Date(), displayedComponents: .date) {
Text("")
}.padding(30)
Text("Date is \(selectedDate, formatter: dateFormatter)")
Spacer()
//Bottom buttons
Text("DONE")
.fontWeight(.semibold)
.frame(width:UIScreen.main.bounds.width/2,height: 60)
.onTapGesture {
self.viewModel.selectedDate = self.selectedDate
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
//calling:
DatePickerView(viewModel: self.viewModel)
Reply against your second question about wrapper properties used in SwiftUI i.e #State, #Binding, #Published.
The most common #Things used in SwiftUI are:
• #State - Binding<Value>
• #Binding - Binding<Value>
• #ObservedObject - Binding<Value> (*)
• #EnvironmentObject - Binding<Value> (*)
• #Published - Publisher<Value, Never>
(*) technically, we get an intermediary value of type Wrapper, which turns a Binding once we specify the keyPath to the actual value inside the object.
So, as you can see, the majority of the property wrappers in SwiftUI, namely responsible for the view’s state, are being “projected” as Binding, which is used for passing the state between the views.
The only wrapper that diverges from the common course is #Published, but:
1. It’s declared in Combine framework, not in SwiftUI
2. It serves a different purpose: making the value observable
3. It is never used for a view’s variable declaration, only inside ObservableObject
Consider this pretty common scenario in SwiftUI, where we declare an ObservableObject and use it with #ObservedObject attribute in a view:
class ViewModel: ObservableObject {
#Published var value: Int = 0
}
struct MyView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View { ... }
}
MyView can refer to $viewModel.value and viewModel.$value - both expressions are correct. Quite confusing, isn’t it?
These two expressions ultimately represent values of different types: Binding and Publisher, respectively.
Both have a practical use:
var body: some View {
OtherView(binding: $viewModel.value) // Binding
.onReceive(viewModel.$value) { value // Publisher
// do something that does not
// require the view update
}
}
Hope it may help you.
You can calculate the current date - 15 days using this:
let previousDate = Calendar.current.date(byAdding: .day, value: -15, to: Date())!
Then use the previousDate in DatePicker`s range:
DatePicker(selection: $selectedDate, in: previousDate...Date(), displayedComponents: .date) { ...
Summing up, your code can look like this:
struct DatePickerView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel: MyViewModel
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}
#State private var selectedDate = Date()
let previousDate = Calendar.current.date(byAdding: .day, value: -15, to: Date())!
var body: some View {
VStack {
//Title
HStack{
Text("SELECT A DATE")
.foregroundColor(.white)
.font(.system(size: 20))
}
.frame(width:UIScreen.main.bounds.width,height: 60)
.background(Color.red)
//Date Picker
DatePicker(selection: $selectedDate, in: previousDate...Date(), displayedComponents: .date) {
Text("")
}.padding(30)
Text("Date is \(selectedDate, formatter: dateFormatter)")
Spacer()
//Bottom buttons
Button(action: {
self.viewModel.selectedDate = self.selectedDate
self.presentationMode.wrappedValue.dismiss()
}) {
Text("DONE")
.fontWeight(.semibold)
}
}
}
}
Tested in Xcode 11.5, Swift 5.2.4.