SwiftUI modal sheet dismisses itself after half a second - ios

I have got a modal sheet, here is the code:
SettingsDashboardView:
#State private var notificationsSettingsSheet = false
var body: some View {
Button(action: {
self.notificationsSettingsSheet.toggle()
}) {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 4) {
Label("Set Daily Reminders", systemImage: "alarm").foregroundColor(Color("TextColor"))
.font(.system(.headline, design: .rounded))
Spacer()
}
}
}
.sheet(isPresented: $notificationsSettingsSheet) {
NotificationSettingsModal()
}
}
NotificationSettingsModal:
var body: some View {
ZStack(alignment: .bottom) {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Text("Daily Reminders")
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.padding(.top, headingTopPadding)
.padding(.horizontal, headingHorizontalPadding).foregroundColor(Color("TextColor"))
Spacer().frame(height: 164)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
Spacer().frame(height: 64)
}
}.background(Color("BackgroundColor").edgesIgnoringSafeArea(.all))
}
When I launch the app and open my sheet, in about 50% of cases sheet dismisses itself after about half a second. If I open sheet after that everything works fine. What can cause this problem?

This will probably not solve the mentioned issue but can be useful for others.
In most cases, this issue happens when the view gets redrawn due to a change in some variables. Be careful that it might be the parent view that have some variables changes.
The best way to debug this kind of behaviour is to use the technique describe here, on Hacking with Swift. The idea is to identify what change caused a view to reload itself by printing print(Self._printChanges()) inside the body property. Note that by doing it, you will temporarily need to add an explicit return.
Then, observer the console and it most cases you will be able to identify the issue and refactor your code.
In my experience (does not seem to be the case here) this often happens when using #Environment(\.editMode) var editMode in both the view and parent view. For some reasons this value changes in both views when presenting a sheet, causing the view to be redrawn and the sheet closed.

I solved this problem by removing the codes below while setting to NavigationView on my homeView this week, which caused my subView's sheet automatically dismissed the first time showing.
NavigationView {...}
// .navigationViewStyle(StackNavigationViewStyle())

Related

ContentView not expanding outside of overlay - SwiftUI

I want to display a view as a popup/tooltip from a view. I beleive the best way to acheive this is by presenting it as an overlay. But, the view is not expanding outside of bounds of where its being presented.
import SwiftUI
struct ContentView: View {
let message = "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)"
var body: some View {
VStack {
Button {
} label: {
Text("Tap Me")
.background(
Rectangle()
.fill(.red)
)
}
.overlay {
contentView
}
}
.padding()
}
var contentView: some View {
Text(message)
.foregroundColor(Color.white)
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 5))
.offset(y: 60)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}
Why is the contentView not expanding outside of the view from where it is being overlayed in my SwiftUI code?
I tried setting fixedSize, frame(maxWidth, but none of them have correct behaviour.
The .overlay modifier always takes the size of its parent view as its maximum size. To put any size view in front of another, you should use a ZStack, e.g.
ZStack {
Button {
} label: {
Text("Tap Me")
.background(
Rectangle()
.fill(.red)
)
}
contentView
}
This is what it looks like (with .opacity applied)

SwiftUI Button Disable styling does not work in some cases

I have the following button in a custom view that I reuse in multiple places
Button(action: { selectedDate = date }) {
VStack {
Text(day.shortName)
.font(.caption2)
.foregroundColor(isSelectedDate ? .white : .primary)
.frame(maxWidth: .infinity)
Spacer().frame(height: 7)
Text("\(date.dayOfMonth)")
.bold()
.foregroundColor(isSelectedDate ? .white : .primary)
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.purple.brightness(isSelectedDate ? 0 : 0.6))
.clipped()
}.disabled(isInPast)
Dates in the past supposed to be disabled, and I confirmed that they are actually disabled as expected; however, the disabled styling looks different in multiple screens although it's the exact same view being used.
What could cause the disable state to not be styled accordingly in some screens?
In both screenshots dates from 25-29 are disabled
In both usages I simply add the view to a VStack
var body: some View {
VStack {
WeekView(selectedDate: $booking.selectedDate).padding()
var body: some View {
VStack(spacing: 0) {
WeekView(selectedDate: $selectedDate)
.padding(.horizontal)
.padding(.bottom)
Don't rely on primary color. Use a custom color and use isInPast to make an opacity.
Figured it out, the parent in one of the screens had .buttonStyle(.plain) which makes the disabled styling work as expected. So I just dded that to the component itself to make sure the disabled styling is always in place

How to have 1 column in a multiple column list be of the same width w/out using a frame modifier of width so to retain flexibility

I have a list of entries that consist of multiple columns of UI with all except the first free to be uniquely sized horizontally (i.e. they’re as short/long as their content demands). I know with the first consistently sized column I can set a frame modifier width to achieve this, but I was hoping there is a better and more flexible way to get the desired behaviour. The reason being I don’t believe the solution is optimised to consider the user’s display size nor the actual max content width of the columns. That is, the width set will either not be wide enough when the display size is set to the largest, or, if it is, then it will be unnecessarily wide on a smaller/regular display size.
This is my current best attempt:
GeometryReader { geometry in
VStack {
HStack {
HStack {
Text("9am")
Image(systemName: "cloud.drizzle").font(Font.title2)
.offset(y: 4)
}.padding(.all)
.background(Color.blue.opacity(0.2))
.cornerRadius(16)
VStack {
HStack {
Text("Summary")
.padding(.trailing, 4)
.background(Color.white)
.layoutPriority(1)
VStack {
Spacer()
Divider()
Spacer()
}
VStack {
Text("12°")
Text("25%")
.foregroundColor(Color.black)
.background(Color.white)
}.offset(y: -6)
Spacer()
}.frame(width: geometry.size.width/1.5)
}
Spacer()
}
HStack {
HStack {
Text("10am")
.customFont(.subheadline)
Image(systemName: "cloud.drizzle").font(Font.title2)
.offset(y: 4)
.opacity(0)
}
.padding(.horizontal)
.padding(.vertical,4)
.background(Color.blue.opacity(0.2))
.cornerRadius(16)
VStack {
HStack {
ZStack {
Text("Mostly cloudy")
.customFont(.body)
.padding(.trailing, 4)
.background(Color.white)
.opacity(0)
VStack {
Spacer()
Divider()
Spacer()
}
}
VStack {
Text("13°")
Text("25%")
.foregroundColor(Color.black)
.background(Color.white)
}.offset(y: -6)
Spacer()
}.frame(width: geometry.size.width/1.75)
}
Spacer()
}
}
}
For me, this looks like:
As you can tell, 10 am is slightly wider than 9 am. To keep them as closely sized as possible, I’m including a cloud icon in it too, albeit with zero opacity. Ideally, 10 am would be sized the same as 9 am without needing a transparent cloud icon. More generally speaking, what would make sense is the widest HStack in this column is identified and then whatever its width is will be applied to all other columns. Keep in mind, my code above is static for demo purposes. It will be a view that is rendered iterating through a collection of rows.
You can use dynamic frame modifiers, such as frame(.maxWidth: .infinity) modifier to extend views so that they fill up the entire frame, even if the frame is dynamic. Here is an example that should help you get going:
struct CustomContent: View {
var body: some View {
VStack {
VStack {
CustomRow(timeText: "9am", systemIcon: "cloud.drizzle", centerText: "Summary", temperature: "12°", percent: "25%")
CustomRow(timeText: "10am", systemIcon: nil, centerText: nil, temperature: "13°", percent: "25%")
}
VStack {
CustomRow(timeText: "9am", systemIcon: "cloud.drizzle", centerText: "Summary", temperature: "12°", percent: "25%")
CustomRow(timeText: "10am", systemIcon: nil, centerText: nil, temperature: "13°", percent: "25%")
}
.frame(width: 300)
}
}
}
struct CustomContent_Previews: PreviewProvider {
static var previews: some View {
CustomContent()
}
}
struct CustomRow: View {
let timeText: String
let systemIcon: String?
let centerText: String?
let temperature: String
let percent: String
var body: some View {
HStack {
//Left column
HStack(alignment: .center) {
Text(timeText)
if let icon = systemIcon {
Image(systemName: icon)
.font(.title2)
}
}
.padding(.all)
.frame(width: 105, height: 60)
.background(Color.blue.opacity(0.2))
.cornerRadius(16)
// Center column
ZStack(alignment: .leading) {
Capsule()
.fill(Color.black.opacity(0.3))
.frame(height: 0.5)
if let text = centerText {
Text(text)
.lineLimit(1)
.background(Color.white)
}
}
// Right column
VStack {
Text(temperature)
Text(percent)
.foregroundColor(Color.black)
}
}
}
}
Guided by https://www.wooji-juice.com/blog/stupid-swiftui-tricks-equal-sizes.html, I accomplished this.
This is the piece of UI I want to make sure is horizontally sized equally across all rows with the width set to whatever is the highest:
HStack {
VStack {
Spacer()
Text("9am")
Spacer()
}
}.frame(minWidth: self.maximumSubViewWidth)
.overlay(DetermineWidth())
The stack the above is contained in has an OnPreferenceChange modifier:
.onPreferenceChange(DetermineWidth.Key.self) {
if $0 > maximumSubViewWidth {
maximumSubViewWidth = $0
}
}
The magic happens here:
struct MaximumWidthPreferenceKey: PreferenceKey
{
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat)
{
value = max(value, nextValue())
}
}
struct DetermineWidth: View
{
typealias Key = MaximumWidthPreferenceKey
var body: some View
{
GeometryReader
{
proxy in
Color.clear
.anchorPreference(key: Key.self, value: .bounds)
{
anchor in proxy[anchor].size.width
}
}
}
}
The link at the top best describes each’s purpose.
MaximumWidthPreferenceKey
This defines a new key, sets the default to zero, and as new values get added, takes the widest
DetermineWidth
This view is just an empty (Color.clear) background, but with our new preference set to its width. We’ll get back to that clear background part in a moment, but first: there are several ways to set preferences, here, we’re using anchorPreference. Why?
Well, anchorPreference has “No Overview Available” so I don’t actually have a good answer for that, other than it seems to be more reliable in practice. Yeah, cargo-cult code. Whee! I have a hunch that, what with it taking a block and all, SwiftUI can re-run that block to get an updated value when there are changes that affect layout.
Another hope I have is that this stuff will get better documented, so that we can better understand how these different types are intended to be used and new SwiftUI developers can get on board without spending all their time on Stack Overflow or reading blog posts like this one.
Anyway, an anchor is a token that represents a dimension or location in a view, but it doesn’t give you the value directly, you have to cash it in with a GeometryProxy to get the actual value, so, that’s what we did — to get the value, you subscript a proxy with it, so proxy[anchor].size.width gets us what we want, when anchor is .bounds (which is the value we passed in to the anchorPreference call). It’s kind of twisted, but it gets the job done.
maximumSubViewWidth is a binding variable passed in from the parent view to ensure the maximumSubViewWidth each subview refers to is always the the up-to-date maximum.
ForEach(self.items) { item, in
ItemSubview(maximumSubViewWidth: $maximumSubViewWidth, item: item)
}
The one issue with this was there was an undesired subtle but still noticeable animation on the entire row with any UI that gets resized to the max width. What I did to work around this is add an animation modifier to the parent container that’s nil to start with that switches back to .default after an explicit trigger.
.animation(self.initialised ? .default : nil)
I set self.initialised to be true after the user explicitly interacts with the row (In my case, they tap on a row to expand to show additional info) – this ensured the initial animation doesn't incorrectly happen but animations go back to normal after that. My original attempt toggled initialised's state in the .onAppear modifier so that the change is automatic but that didn't work because I’m assuming resizing can occur after the initial appearance.
The other thing to note (which possibly suggests although this solution works that it isn't the best method) is I'm seeing this message in the console repeated for either every item, or just the ones that needed to be resized (unclear but the total number of warnings = number of items):
Bound preference MaximumWidthPreferenceKey tried to update multiple
times per frame.
If anyone can think of a way to achieve the above whilst avoiding this warning then great!
UPDATE: I figured the above out.
It’s actually an important change because without addressing this I was seeing the column keep getting wider on subsequent visits to the screen.
The view has a new widthDetermined #State variable that’s set to false, and becomes true inside .onAppeared.
I then only determine the width for the view IF widthDetermined is false i.e. not set. I do this by using the conditional modifier proposed at https://fivestars.blog/swiftui/conditional-modifiers.html:
func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> TupleView<(Self?, Content?)> {
if conditional { return TupleView((nil, content(self))) }
else { return TupleView((self, nil)) }
}
and in the view:
.if(!self.widthDetermined) {
$0.overlay(DetermineWidth())
}
I had similar issue. My text in one of the label in a row was varying from 2 characters to 20 characters. It messes up the horizontal alignment as you have seen. I was looking to make this column in row as fixed width. I came up with something very simple. And it worked for me.
var body: some View { // view for each row in list
VStack(){
HStack {
Text(wire.labelValueDate)
.
.
.foregroundColor(wire.labelColor)
.fixedSize(horizontal: true, vertical: false)
.frame(width: 110.0, alignment: .trailing)
}
}
}

What is the correct way to pass touches through a view to those below it?

I have a SwiftUI view that is displayed over other views, and have found that using Color.clear like this below seems to allow touch interactions to pass through to anything under it:
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
SomeCustomContent()
Spacer()
}
.overlay(GeometryReader { proxy in
Color.clear.preference(key: MyCustomHeightPreferenceKey.self, value: proxy.size.height)
})
}
}
Is this the correct way to make touches pass through to the views below, or it this just a coincidental quirk/bug in SwiftUI behaviour that Apple might fix/change as swiftui matures?
If not, what is the correct way to pass the touches through?
You can pass through touch events without use a clear color like this:
var body: some View {
Rectangle()
.overlay(
Circle()
.fill(.blue)
.allowsHitTesting(false) // <--- Passes through gestures
)
}
Asperi mentioned this solution in a comment above, and you can also find a good blog about this topic here: https://www.hackingwithswift.com/books/ios-swiftui/disabling-user-interactivity-with-allowshittesting

How to show a view when tapping on a list item?

I am working with a list in SwiftUI, I am attempting to recreate a system I had with TableView whereby a user can tap a cell and then a new view is presented with data relating to said cell. Now we have lists my code has changed to the following:
List {
ForEach(clients, id: \.id) { client in
VStack(alignment: .center) {
HStack{
Text(client.firstName ?? "Unknown" + " ")
.font(.system(size: 17))
.foregroundColor(Color.init(hex: "47535B"))
.fontWeight(.medium)
.multilineTextAlignment(.leading)
.padding(.leading)
Text(client.lastName ?? "Unknown")
.font(.system(size: 17))
.foregroundColor(Color.init(hex: "47535B"))
.fontWeight(.medium)
.multilineTextAlignment(.leading)
Spacer()
}
}
.frame(height: 50.0)
.background(Color.init(hex: "F6F6F6"))
.cornerRadius(7.0)
}
}
.padding(.horizontal, 3.0)
.padding(.vertical, 115.0)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
I realise I can use a NavigationLink and place the entire thing into a NavigationView but this functionality is not what I want, below is an image of my interface. What I am trying to achieve is when the user taps a cell it presents the data in the space on the right where it says "Select a client to view their profile". With the NavigationView setup I can only use the 2 default styles neither of which are suitable for me since I cannot customise where the navigation view gets placed. Is there a way I can register the same tap but have my own custom system for displaying the resulting data where I want in my interface? Perhaps I am wrong about NavigationView or maybe there is a way to have the NavigationView be positioned entirely outside of the view that contains the item list?
If you don't want to or aren't using use a NavigationView, then you likely have both the client list and the client detail in the same view somewhere. I would try adding #State private var selectedClient: Client? = nil to whatever view has both the list and the detail.
First, pass selectedClient as a binding to the list. Next, whenever one of the list items is tapped (achievable through .onTapGesture() or Button), Update selectedClient.
In your detail view, accept a bindable Client? parameter. If it's nil, then just show your current Text view. If it's not nil, then build the detail UI.
Hope this helps!

Resources