Problem
I am trying to create two pill-like, circular buttons using the following code. On iPadOS & iOS, everything works fine. However, if I put the preview device to my MacBook, things start glitching as depicted on the picture.
This is the according code:
struct ButtonBar: View {
var body: some View {
HStack(alignment: .center) {
Spacer()
Button {
// action not yet implemented
} label: {
Text("Cancel")
.font(FontFamily.CustomFont.regular.swiftUIFont(fixedSize: 16))
.foregroundColor(Color.white)
.padding(EdgeInsets(top: 12, leading: 39, bottom: 12, trailing: 39))
.background(Color(Theme.CustomGrayColor))
.clipShape(Capsule(style: .circular))
}
Button {
// action not yet implemented
} label: {
Text("Share")
.font(FontFamily.CustomFont.regular.swiftUIFont(fixedSize: 16))
.foregroundColor(Color(Theme.CustomGrayColor))
.padding(EdgeInsets(top: 12, leading: 39, bottom: 12, trailing: 39))
.background(Color(Theme.CustomOrangeColor))
.clipShape(Capsule(style: .circular))
}
}
.padding(EdgeInsets(top: 24, leading: 0, bottom: 24, trailing: 24))
.background(
Color(Theme.CustomGrayColor2)
.shadow(color: Color.black.opacity(0.25), radius: 12, x: 0, y: 0)
)
}
}
struct ButtonBar_Previews: PreviewProvider {
static var previews: some View {
ButtonBar()
}
}
Now, this is where the weird part comes in: on iOS, everything works perfectly fine using above code, and the buttons look like this:
Question
I was expecting that SwiftUI displays things correctly on both platforms automatically (isn't that the biggest benefit of SwiftUI vs. UIKit/AppKit?).
How can I solve this issue and make the buttons on macOS look just like the buttons on iOS, without creating a separate view for the different platforms?
Approach
Use .buttonStyle(.plain)
Best to follow Apple's HIG (design guidelines)
The button you are attempting is not common on macOS.
IMHO I feel (I could be wrong) it doesn't adhere to the design guidelines for the macOS.
Suggestion: Reduce the corner radius for the macOS
Simplified version of your button
I still to prefer to use the default button for the Mac.
This would look odd on the Mac, but since you asked it is below
struct ContentView: View {
var body: some View {
Button {
print("pressed")
} label: {
Text("some button")
.padding(12)
.background(in: RoundedRectangle(cornerRadius: 10))
.backgroundStyle(.brown)
}
.buttonStyle(.plain)
}
}
Related
I'm trying to create a bottom sheet in swift ui that looks something like this
https://user-images.githubusercontent.com/33900517/172068237-4dd58374-b6e6-4340-a913-7085fb64b254.mp4
My issue is that I have an animated bottom sheet, but because it is ignoring the safe area, when I click into the textfield it does not expand with the keyboard.
How would I fix this so the view expands with the keyboard but the white at the bottom still goes beyond the safe area?
I.e. the containing view should ignore the safe area, and the content within should adhere to the safe area.
Here is the bottom sheet code snippet, full example can be found here
https://gist.github.com/CTOverton/4fbfb8db2de31f3b5f5ef9ee88e8f744
var body: some View {
GeometryReader { geometry in
VStack() {
self.content
}
.padding(.vertical, 34)
.padding(.horizontal, 16)
// .frame(width: geometry.size.width, height: geometry.size.height * heightRatio, alignment: .top)
.frame(width: geometry.size.width, height: self.maxHeight, alignment: .top)
.background(Color(.white))
.cornerRadius(Constants.radius)
.frame(height: geometry.size.height, alignment: .bottom)
.offset(y: max(self.offset + self.translation, 0))
.animation(.interactiveSpring())
.gesture(
DragGesture().updating(self.$translation) { value, state, _ in
state = value.translation.height
}.onEnded { value in
let snapDistance = self.maxHeight * Constants.snapRatio
guard abs(value.translation.height) > snapDistance else {
return
}
self.isOpen = value.translation.height < 0
}
)
}
.edgesIgnoringSafeArea([.bottom, .horizontal])
.shadow(color: Color(hue: 1.0, saturation: 0.0, brightness: 0.0, opacity: 0.08), radius: 12, y: -8)
}
I've tried various configurations of .ignoreSafeArea() and .safeAreaInset() but I just can't seem to get it quite right.
Here are some pictures for reference as well
Actually instead of ignoring safe area for everything (that results in issue), we need it only in background, so the question is how to correctly construct background in this case.
Note: the .cornerRadius is also not appropriate here, because it clips content
Here is a main part of a fix. Tested with Xcode 13.4 / iOS 15.5
.background(
RoundedRectangle(cornerRadius: Constants.radius) // corners !!
.fill(.white) // background !!
.edgesIgnoringSafeArea([.bottom, .horizontal]) // << only here !!
)
Complete test module is here
I have in my app a layout showing a list of rectangular cards - each one should be tappable (once) to reveal a set of action buttons and more information, etc.
I have implemented this using .onTapGesture() and I have also put .contentShape(Rectangle() to enforce the tappable area. However, while my implementation works fine for touchscreen interface, when I'm using it with the iPadOS mouse support, and on Catalyst for that matter, I see some very unexpected behaviour.
I've made a minimal reproducible example below that you can copy to recreate the problem.
Where the problems are when using mouse/trackpad input:
Not every click of the mouse is recorded as a tap gesture. This is happening mostly arbitrary except from in a few cases:
It seems to click either only in very specific areas, or when clicking multiple times in the same spot.
It seems that in a lot of cases only every other click is recorded. So I double click to get only one tap gesture.
It isn't evident in this example code, but in my main app the tappable areas are seemingly arbitrary - you can usually click near text or in alignment with it to record a tap gesture, but not always.
If you are running the example code you should be able to see the problem by repeatedly moving the mouse and attempting one click. It doesn't work unless you click multiple times in the same spot.
What does work as expected:
All input using touch instead of mouse; regardless of where you tap it records a tap gesture.
Mouse input when running as a native Mac target. The issues mentioned above are only for mouse/trackpad when running the example under iPadOS and Mac Catalyst.
Code I used to recreate this problem (has a counter to count every time a tap gesture is recorded):
struct WidgetCompactTaskItemView: View {
let title: String
let description: String
var body: some View {
HStack {
Rectangle()
.fill(Color.purple)
.frame(maxWidth: 14, maxHeight: .infinity)
VStack(alignment: .leading) {
Text(title).font(.system(size: 14, weight: .bold, design: .rounded))
Text(description).font(.system(.footnote, design: .rounded))
.frame(maxHeight: .infinity)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.padding(.vertical, 0.1)
Spacer()
}
.padding(.horizontal, 6)
.padding(.top, 12)
}
.frame(maxWidth: .infinity, maxHeight: 100, alignment: .leading)
.background(Color.black)
.cornerRadius(16)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.green, lineWidth: 0.5)
)
}
}
struct ContentView: View {
#State var tapCounter = 0
var body: some View {
VStack {
Text("Button tapped \(tapCounter) times.")
WidgetCompactTaskItemView(title: "Example", description: "Description")
.contentShape(Rectangle())
.onTapGesture(count: 1) {
tapCounter += 1
}
Spacer()
}
}
}
I have tried several things including moving modifiers around, setting eoFill to true on the contentShape modifier (which didn't fix the problem but simply made different unexpected behaviour).
Any help to find a solution that works as expected and works consistently whether mouse or touch would be much appreciated. I am not sure if I am doing something wrong or if there is a bug here, so please try and recreate this example yourself using the code to see if you can reproduce the problem.
So I realised that there was a much better solution that could bypass all the oddities that .onTapGesture had for me with mouse input. It was to encapsulate the whole view in a Button instead.
I made this into a modifier similar to onTapGesture so that it's much more practical.
import Foundation
import SwiftUI
public struct UltraPlainButtonStyle: ButtonStyle {
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
}
}
struct Tappable: ViewModifier {
let action: () -> ()
func body(content: Content) -> some View {
Button(action: self.action) {
content
}
.buttonStyle(UltraPlainButtonStyle())
}
}
extension View {
func tappable(do action: #escaping () -> ()) -> some View {
self.modifier(Tappable(action: action))
}
}
Going through this:
I first have a button style which simply returns the label as is. This is necessary because the default PlainButtonStyle() still has a visible effect when clicked.
I then create a modifier that encapsulates the content given in a Button with this button style, then add that as an extension to View.
Usage example
WidgetCompactTaskItemView(title: "Example", description: "Description")
.tappable {
tapCounter += 1
}
This has solved all problems I've been having with clickable area using a mouse.
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())
I'm trying to get items inside a list to line up in a specific way.
List {
HStack {
Text("1.")
Text("Item 1")
}
HStack {
Text("Item 2")
}
}
That winds up looking like this:
1. Item 1
Item 2
What I'd like is to line up, in this example, "Item 1" and "Item 2":
1. Item 1
Item 2
That is, the "item" parts all line up whether they have a list marker or not, or if they have list markers of different lengths (number 1. lines up with 100.)
I tried making a custom alignment guide as seen here but these don't seem to be respected inside a List --- it works fine if I make the AlignmentGuide and put it all in a VStack, but I need list behavior.
(I could fake this by getting rid of the HStacks and doing Text("1.\tItem 1") and Text("\tItem 2"). The tab stops would make everything line up, but I need to apply different formatting to the list marker and the list item (bolding, color, etc.), so they need to be discrete Text elements.)
Any ideas would be appreciated.
import SwiftUI
struct ContentView: View {
var body: some View {
List {
HStack {
Text("1.").frame(width: 20.0, height: nil, alignment: .leading)
Text("Item 1")
}
HStack {
Text("Item 2")
.padding(EdgeInsets(top: 0, leading: 28, bottom: 0, trailing: 0))
}
HStack {
Text("2.").frame(width: 20.0, height: nil, alignment: .leading)
Text("Item 3")
}
HStack {
Text("Item 4")
.padding(EdgeInsets(top: 0, leading: 28, bottom: 0, trailing: 0))
}
}
}
}
** Updated **
Hope this is closer to what you are looking for.
By specifying a frame around the leading value, you can control its size so it should work for your need to modify the text value.
It should also be possible to calculate values for the purpose of setting the frame and padding, but these hard coded values should achieve the immediate effect.
I know tons of people already thought about this, however I could't find any good answer on Stackoverflow or a good tutorial that purely talk about this even on some famous channels in Youtube.
My question is very simple:
In SwiftUI we may do tons of time like this:
Text("Hello World")
.padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3))
.background(Color.blue)
.cornerRadius(5)
However, as you, or any other very experienced and professional developers may aware, we absolutely don't want to write .padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3)).background(Color.blue).cornerRadius(5) for every "Text(...)" or other like "Button" or any other SwiftUI component. we do want to wrap the .padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3)).background(Color.blue).cornerRadius(5)
in some kind of methods or somewhere else. I know how to do it in UIKit, but the question is how do we do that since SwiftUI is a declarative way of build GUIs?
Thanks.
Here is how it is usually done with custom ViewModifier, plus depicts example of how it could be configured via parameters:
struct MyTextModifier: ViewModifier {
let corner: CGFloat
func body(content: Content) -> some View {
content
.padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3))
.background(Color.blue)
.cornerRadius(corner)
}
}
extension View {
func configured(with radius: CGFloat = 5) -> some View {
self.modifier(MyTextModifier(corner: radius))
}
}
struct MyTextModifier_Previews: PreviewProvider {
static var previews: some View {
Text("Hello World")
.configured()
}
}
Tested with Xcode 11.2, iOS 13.2
The easiest way to group modifiers for reuse is to declare them in an extension on View:
extension View {
func customStyle() -> some View {
self.padding(3)
.background(Color.blue)
.cornerRadius(5)
}
}
then you can just simply use it like this:
Text("Hello World").customStyle()
This will only work when all of the modifiers can be applied to View. When you want to group modifiers specific to Image you would have to do it in an extension on Image etc.
I would create every component as a view, instead of an extension. That way you can always preview the component and have a central repository of 'reusable' components:
struct customText : View {
let text: String
var body: some View {
Text(string)
.padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3))
.background(Color.blue)
.cornerRadius(5)
}
}
Append a preview to the above and you can always view exactly what your text will look like in the preview canvas, making future edits easier.
And to use:
customText(text: "Hello World")
You can still add modifiers to its use. You can then have a single source of views which can be used throughout your app (different text types, buttons, etc).