UICalendarView vs SwiftUI DatePicker wrong size in NavigationSplitView sidebar - swiftui

Because i need to add badges to certain dates, i need to use a UICalendarView representable, however when using this in the NavigationSplitView sidebar, the width of the UICalendarView is incorrect... in contrast the graphical DatePicker which should be the same component, is layout correctly
In the detail pane the layout the same, the problem arises with less horizontal space
How to fix this?
struct CalendarUI: UIViewRepresentable {
let interval: DateInterval
func makeUIView(context: Context) -> UICalendarView {
let view = UICalendarView()
view.calendar = Calendar.current
view.availableDateRange = interval
return view
}
func updateUIView(_ uiView: UICalendarView, context: Context) {
}
}
struct SidebarUI: View {
#State private var date = Date()
var body: some View {
VStack {
CalendarUI(interval: DateInterval(start: .distantPast, end: .distantFuture))
.fixedSize(horizontal: true, vertical: true)
DatePicker(
"Start Date",
selection: $date,
displayedComponents: [.date]
)
.datePickerStyle(.graphical)
}.padding()
}
}

Related

SwiftUI changing navigation bar background color for inline navigationBarTitleDisplayMode

I just started coding in SwiftUI and came across a problem. I need to give different colors to the background of the navigation bar (NavigationView). The colors will change as I go from one view to the next. I need to have this working for navigationBarTitleDisplayMode being "inline".
I tried the solutions presented in:
SwiftUI update navigation bar title color
but none of these solutions work fully for what I need.
The solution in this reply to that post works for inline:
Using UIViewControllerRepresentable. Nevertheless, when we first open the view it will show the color of the previous view for one second, before changing to the new color. I would like to avoid this and have the color displayed as soon as everything appears on screen. Is there a way to do this?
This other solution will not work either: Changing UINavigation's appearance in init(), because when I set the background in init(), it will change the background of all the views in the app. Again, I need the views to have different background colors.
I tried something similar to this solution: Modifying Toolbar, but it does not allow me to change the color of the navigation bar.
The other solution I tried was this: Creating navigationBarColor function, which is based on: NAVIGATIONVIEW DYNAMIC BACKGROUND COLOR IN SWIFTUI. This solution works for navigationBarTitleDisplayMode "large", but when setting navigationBarTitleDisplayMode to "inline", it will show the background color of the navigation bar in a different color, as if it was covered by a gray/transparent layer. For example, the color it shows in "large" mode is:
Red color in large mode
But instead, it shows this color:
Red color in inline mode
Finally, I tried this solution: Subclassing UIViewController and configuring viewDidLayoutSubviews(), but it did not work for what I want it either.
The closest solutions for what I need are 1. and 4., but they still do not work 100%.
Would anybody know how to make any of these solutions work for navigationBarTitleDisplayMode inline, being able to change the background color of the navigation bar in different layouts, and showing the new color once the view is shown (without delays)?
Thank you!
By the way, I am using XCode 12.5.
Here is the sample code that I am using, taking example 4. as a model:
FirstView.swift
import SwiftUI
struct FirstView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
GeometryReader { metrics in
VStack {
Text("This is the first view")
NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "SecondView"
print("Go to second view")
}) {
Text("Go to second view")
}
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
On this screen, if I use
.navigationBarTitleDisplayMode(.large)
the color will be displayed properly: Navigation bar with red color
But using
.navigationBarTitleDisplayMode(.inline)
there is a blur on it: Navigation bar with some sort of blur over red color
import SwiftUI
struct SecondView: View {
#State private var selection: String? = nil
var body: some View {
GeometryReader { metrics in
VStack {
Text("This is the second view")
NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "ThirdView"
print("Go to third view")
}) {
Text("Go to third view")
}
}
}
.navigationBarColor(backgroundColor: Color.red, titleColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ThirdView.swift
This view displays the color properly as it is using
.navigationBarTitleDisplayMode(.large)
But if changed to
.navigationBarTitleDisplayMode(.inline)
it will show the blur on top of the color as well.
import SwiftUI
struct ThirdView: View {
var body: some View {
GeometryReader { metrics in
Text("This is the third view")
}
.navigationBarColor(backgroundColor: Color.blue, titleColor: .black)
.navigationBarTitleDisplayMode(.large)
}
}
struct ThirdView_Previews: PreviewProvider {
static var previews: some View {
ThirdView()
}
}
NavigationBarModifierView.swift
import SwiftUI
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: Color, titleColor: UIColor?) {
self.backgroundColor = UIColor(backgroundColor)
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = UIColor(backgroundColor)
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = titleColor
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
NOTE TO THE MODERATORS: Please, do not delete this post. I know similar questions were asked before, but I need an answer to this in particular which was not addressed. Please read before deleting indiscriminately, I need this for work. Also, I cannot ask questions inline in each of those solutions because I do not have the minimum 50 points in stackoverflow required to write there.
I think I have what you want. It is VERY touchy... It is a hack, and not terribly robust, so take as is...
I got it to work by having your modifier return a clear NavBar, and then the solution from this answer works for you. I even added a ScrollView to ThirdView() to make sure that scrolling under didn't affect in. Also note, you lose all of the other built in effects of the bar like translucency, etc.
Edit: I went over the code. The .navigationViewStyle was in the wrong spot. It likes to be outside of the NavigaionView(), where everything else needs to be inside. Also, I removed the part of the code setting the bar color in FirstView() as it was redundant and ugly. I hadn't meant to leave that in there.
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: Color, titleColor: UIColor?) {
self.backgroundColor = UIColor(backgroundColor)
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = .clear // The key is here. Change the actual bar to clear.
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = titleColor
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
struct FirstView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
GeometryReader { _ in
VStack {
Text("This is the first view")
NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "SecondView"
print("Go to second view")
}) {
Text("Go to second view")
}
}
.navigationTitle("First")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .red, titleColor: .black)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SecondView: View {
#State private var selection: String? = nil
var body: some View {
VStack {
Text("This is the second view")
NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "ThirdView"
print("Go to third view")
}) {
Text("Go to third view")
}
}
.navigationTitle("Second")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .blue, titleColor: .black)
}
}
struct ThirdView: View {
var body: some View {
ScrollView {
ForEach(0..<50) { _ in
Text("This is the third view")
}
}
.navigationTitle("Third")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .green, titleColor: .black)
}
}
iOS 16
Since this version of SwiftUI, there is a dedicated modifier for setting any toolbar background color (including the navigation bar):
Xcode 14 beta 5 (Not working 🤦🏻‍♂️, waiting for beta 6...)
.toolbarBackground(.red, for: .navigationBar)
Xcode 14 beta 1,2,3,4
.toolbarBackground(.red, in: .navigationBar)
It works perfectly in in inline mode and also animates between modes.
For my custom view the following code worked well.
struct HomeView: View {
init() {
//Use this if NavigationBarTitle is with Large Font
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
//Use this if NavigationBarTitle is with displayMode = .inline
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
UINavigationBar.appearance().backgroundColor = UIColor.clear
UINavigationBar.appearance().barTintColor = UIColor(Color(red: 32 / 255, green: 72 / 255, blue: 63 / 255))
}
var body: some View {
NavigationView {
ZStack {
...
...
...
}
.padding(.zero)
.navigationTitle("Feedbacks")
}
}
}
and result is like that:
Here is a bit hacky solution, but it works for me (as of iOS 15) both for .large and .inline display modes.
import SwiftUI
enum Kind: String, CaseIterable {
case checking
case savings
case investment
}
struct PaddedList: View {
#Binding var name: String
#Binding var kind: Kind
var body: some View {
NavigationView {
List {
TextField("Account name", text: $name)
Picker("Kind", selection: $kind) {
ForEach(Kind.allCases, id: \.self) { kind in
Text(kind.rawValue).tag(kind)
}
}
.listRowSeparatorTint(.red)
Spacer()
}
.padding(.top, 1) // note top 1 padding!
.background(.green) // the color "bleeds" through
.navigationBarTitle("Navigation Bar")
}
}
}
struct PaddedList_Previews: PreviewProvider {
static var previews: some View {
PaddedList(name: .constant(""), kind: .constant(.checking))
}
}

Is it possible to dismiss the DatePicker's calendar when you tap a date?

I have a basic SwiftUI date picker that shows a calendar widget when tapped:
DatePicker(
"Date",
selection: $date,
in: ...Date(),
displayedComponents: [.date]
)
When you select a date (8th October in the example above), the calendar remains on screen and in order to collapse it, you need to tap outside of it.
Is it possible to automatically collapse it when a date is selected?
I ended up with a rather hacky solution that seems to do the job:
Add a #State variable that holds the calendar ID:
#State private var calendarId: Int = 0
Chain the DatePicker call with .id, .onChange and .onTapGesture actions:
DatePicker(
"Date", selection: $date, in: ...Date(), displayedComponents: [.date]
)
.id(calendarId)
.onChange(of: date, perform: { _ in
calendarId += 1
})
.onTapGesture {
calendarId += 1
}
#chris.kobrzak provided a good direction, and I ended up solving this with:
struct ContentView: View {
#State var calendarId: UUID = UUID()
#State var someday: Date = Date()
var body: some View {
VStack {
DatePicker("Day", selection: $someday, displayedComponents: [.date])
.labelsHidden()
.id(calendarId)
.onChange(of: whatday) { _ in
calendarId = UUID()
}
AnotherView(someday)
}
}
}
This is just an updated answer following #Chris Kobrzak as above.
I am using XCode 14.1 and iOS 15+ and 16+ (iPad and iPhone) and it seems to work without error today in Nov 2022.
I have seen some folk using the same .id() method complain that it doesn’t work.
I haven’t tested this but note that I am using the CompactDatePickerStyle(), maybe it doesn’t work the same on other styles.
The reason this hack works is the .id() is for the ‘view’ (DatePicker being a view). When you change the id of a view you basically reset it (in this case closing the DatePicker).
There is a good explanation about .id() here: https://swiftui-lab.com/swiftui-id/
Why this isn’t built into the control seems rather a joke but hey…
Note I have ripped the following out of a real App. I've edited it in a dumb text editor to post on here so there may be some silly syntax errors and odd remnants of the original code.
import SwiftUI
struct FooView: View {
#Published var dateOfBirth: Date = Date()
#State private var datePickerId: Int = 0
private var dateOfBirthRange: ClosedRange<Date> {
let dateFrom = Calendar.current.date(byAdding: .year, value: -160, to: Date())!
let dateTo: Date = Date()
return dateFrom...dateTo
}
var body: some View {
Form {
ZStack(alignment: .leading) {
Text("Date of Birth")
.offset(y: -36)
.foregroundColor(Color.accentColor)
.scaleEffect(0.9, anchor: .leading)
DatePicker(
"",
selection: $dateOfBirth,
in: dateOfBirthRange,
displayedComponents: .date
)
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
.id(datePickerId)
.onChange(of: dateOfBirth) { _ in
datePickerId += 1
}
}
.padding(.top, 24)
.animation(.default, value: "")
}
}
}
I had a similar problem and put a .graphical DatePicker in my own popover. The only downside is on iPhone popovers currently show as sheets but that's ok.
struct DatePickerPopover: View {
#State var showingPicker = false
#State var oldDate = Date()
#Binding var date: Date
let doneAction: () -> ()
var body: some View {
Text(date, format:.dateTime.year())
.foregroundColor(.accentColor)
.onTapGesture {
showingPicker.toggle()
}
.popover(isPresented: $showingPicker, attachmentAnchor: .point(.center)) {
NavigationStack {
DatePicker(selection: $date
, displayedComponents: [.date]){
}
.datePickerStyle(.graphical)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
date = oldDate
showingPicker = false
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
doneAction()
showingPicker = false
}
}
}
}
}
.onAppear {
oldDate = date
}
}
}

SwiftUI - making a textfield inside a view be the first responder on touch to that view

On a view I have something like this:
TextFieldUsername()
this shows something like
So, this view shows an icon and the textfield username.
Below that, I have another one for the password.
Making that username field in focus is unnecessarily hard. The textfield is not small, but making the username field to focus is a matter of tapping on the exact position and perhaps you have to tap 2 or 3 times to make it happen.
I would like to make the whole TextFieldUsername() tappable or to increase the hit area of that textfield. I would like better to make the whole thing tappable and once tapped, make its textfield in focus.
This is TextFieldUsername
struct TextFieldUsername: View {
#State var username:String
var body: some View {
HStack {
Image(systemName: "person.crop.circle")
.renderingMode(.template)
.foregroundColor(.black)
.opacity(0.3)
.fixedSize()
TextField(TextFieldUsernameStrings.username, text: $username)
.textFieldStyle(PlainTextFieldStyle())
.textContentType(.username)
.autocapitalization(.none)
}
}
}
Is that possible in SwiftUI without using any external library like introspect?
Using a custom TextField like the one from Matteo Pacini
you can do something like this:
struct CustomTextField1: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var didBecomeFirstResponder = false
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
#Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField1>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField1.Coordinator {
return Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField1>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
struct ContentView : View {
#State var text: String = ""
#State var isEditing = false
var body: some View {
CustomTextField1(text: $text, isFirstResponder: isEditing)
.frame(width: 300, height: 50)
.background(Color.red)
.onTapGesture {
isEditing.toggle()
}
}
}
It's a bit complex, but should get the work done. As for a pure SwiftUI answer, it's currently unavailable.

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?

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.