Is it possible to have overlapping views that don't blend? - swiftui

I'm trying to mimic the grouped Cancel / Set button pair that you see in places like the Stopwatch app.
I've currently done this by using a ZStack with two overlapping RoundedRectangle with different cornerRadius and padding.
This seems to work well shape-wise but there's a subtle colour overlap that I haven't found a way to fix.
I've tried playing with BlendMode and `opacity' with no luck.
Button(action: {}, label: { Text("Cancel").foregroundColor(Color.white) })
.background(VStack(spacing: 0) {
ZStack {
RoundedRectangle(cornerRadius:20).foregroundColor(Color.gray)
RoundedRectangle(cornerRadius: 8).foregroundColor(Color.gray).padding(.leading, 20)
}})
Does anyone have any ideas?

I've realised that this is actually the button's accent colour being applied. If I set the accentColour to Color.clear, this overlap disappears.
I actually need this behaviour elsewhere so I've followed Alejandro Martinez's guide on creating reusable Button styles and created this:
public struct AccentlessButtonStyle: ButtonStyle {
public func body(configuration: Button<Self.Label>, isPressed: Bool) -> some View {
configuration.accentColor(Color.clear)
}
}
extension StaticMember where Base: ButtonStyle {
public static var accentless: AccentlessButtonStyle.Member {
StaticMember<AccentlessButtonStyle>(AccentlessButtonStyle())
}
}
It seems to inherit the default button styling in all other ways.
Using it requires this:
Button(action: {}, label: { Text("Cancel") }).buttonStyle(.accentless)

Related

SwiftUI: allow simultaneous gestures with `onDrag` in view?

I am testing out the solution provided by Asperi here:
SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?
so,
what I would like to do is to add a contextMenu on a cell, and if the user chooses to reorder cells, then would enable the onDrag and onDrop gestures.
I looked into the simultaneousGesture but, I am not sure how trigger the contextMenu while the view also has onDrag attached to it.
so I tried something like this:
...............
ScrollView(.vertical, showsIndicators: true) {
LazyVGrid(columns: gridItems, spacing: 0) {
ForEach(model.data) { d in
GridItemView(d: d)
.frame(width: side, height: side)
.overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
.contextMenu {
Button {
editCells.toggle()
} label: {
Label("Edit...", systemImage: "pencil")
}
// other options .....
}
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.disabled(!editCells)
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
.disabled(!editCells)
}
}
.animation(.default, value: model.data)
...............
So, the contextMenu never triggers unless I remove the onDrag and onDrop gestures first.
Is there a way to use the contextMenu to "enter" an edit mode to allow drag and drop? Is there a way to make it work while the onDrag and onDrop gestures are installed on the view?
Asperi is right. For the curious as to how I got around doing this I used this conditional View extension:
extension View {
/// Applies the given transform if the given condition evaluates to `true`.
/// - Parameters:
/// - condition: The condition to evaluate.
/// - transform: The transform to apply to the source `View`.
/// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
#ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
The both use same gesture (LongPress) for activation, so there is direct conflict. A possible approach is to make onDrag conditional (toggled by some external command).
The Draggable/canDrag can be similar as Droppable/acceptDrop in https://stackoverflow.com/a/61081772/12299030.

Is there a way to set font tracking (i.e. spacing) inside a custom SwiftUI ButtonStyle?

In SwiftUI you can set font tracking (spacing between letters) on a Text View using the tracking View modifier:
Text("Hello, World!")
.tracking(10)
If you have a Button and apply your custom ButtonStyle you can do all kinds of modifications to the style of the button, but since the configuration.label is not an instance of Text but rather ButtonStyleConfiguration.Label, I don't have direct access to apply tracking. Is there a way to do this without having to set it on the Button's label View directly? It seems like something you ought to be able to do since it's a style-related thing but I don't see how to accomplish it.
public struct MyStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
configuration.label
// (if I tried to put `.tracking` here it wouldn't work because it isn't Text)
.foregroundColor(Color.red)
.clipShape(Capsule())
// … etc.
}
}
ViewModifier to change both font and tracking looks ~ related but no relevant answer.
Yes, ButtonStyleConfiguration.Label is not matched to Text, but it is your style you can ignore standard label, and request own by interface contract, exactly which you need (Text in this case), like in below demo
public struct MyStyle: ButtonStyle {
let label: Text // << here !!
public func makeBody(configuration: Configuration) -> some View {
label // << here !!
.tracking(10)
.foregroundColor(configuration.isPressed ? .black : .red)
.clipShape(Capsule())
}
}
and use it as (or create own button extension with button builder to hide those unneeded curls)
Button(action: {
// action here
}){}
.buttonStyle(MyStyle(label: Text("Hello")))
Tested with Xcode 13.2 / iOS 15.2

Custom ToolTip in swiftUI for iOS

I have a row of colors and I want to display a ToolTip for each of the colors.
struct ColorsView: View {
let colors = [UIColor.red, UIColor.white, UIColor.gray, UIColor.blue, UIColor.black]
var body: some View {
HStack {
ForEach(0..<colors.count) { index in
Color(self.colors[index])
.frame(width: (UIScreen.main.bounds.size.width - 30) / 5, height: 25)
}
}.cornerRadius(12)
}
}
How can I create a custom toolTip for this? I tried wrapping in a ZStack but this doesn't seem to solve the problem exactly. Any help :)
You can also try using this tool: https://github.com/quassummanus/SwiftUI-Tooltip
Here's an example of how you could use it with a simple Text view, you can extrapolate from there. It's very nice because it doesn't rely on any Apple-provided APIs that are not fundamental to SwiftUI, so you can use it on all platforms.
Text("Say something nice...")
.tooltip(.bottom) {
Text("Something nice!")
}
And you get something like this:

How to get a GeometryReader to work only in one dimension?

I am using a Geometry Reader to control the horizontal layout of some child views in a HStack. The child views must finally be equally spaced and their numbers are not fixed. This works fine without any problems.
However, the vertical layout, which is solely determined by the content (child views), is expanded to max available height by the geometry reader (which is standard behaviour).
Any suggestions on how to overcome this behaviour are welcome?
(Another way of explaining the problem is to say that a Horizontal Geometry Reader would solve the problem)
Hereby a code snippet added for clarity:
public struct ToolbarView: View {
#ObservedObject public var viewModel: ToolbarViewModel
public var body: some View
{
GeometryReader { geometry in
HStack(spacing:0) {
ForEach(self.viewModel.items.filter({ $0.visible })) { item in
ToolbarItemView(itemViewModel:item, toolbarViewModel: self.viewModel)
.frame(width:geometry.size.width / CGFloat(Double(self.viewModel.items.count)))
.font(self.viewModel.textFont)
}
}
}.frame(height:CGFloat(50))
}
}
As you note in the comments, Spacers will fix this without a GR. You just need to put them in an HStack:
HStack(spacing:0) {
ForEach(self.viewModel.items.filter({ $0.visible })) { item in
HStack {
Spacer()
ToolbarItemView(itemViewModel:item, toolbarViewModel: self.viewModel)
.font(self.viewModel.textFont)
Spacer()
}
}.frame(height:CGFloat(50))
}

How to create slot-machine metaphor in SwiftUI Picker?

Is it possible to create a slot machine with swiftUI to show two sets of values?
In UIKit, UIPickerView provides the option to have multiple components in your picker view. SwiftUI's Picker does not. However, you can use more than one Picker in an HStack instead. The perspective may look slightly different than a UIPickerView with multiple components in some instances, but to me it looks perfectly acceptable.
Here's an example of a slot machine with 4 pickers side by side and a button that "spins" the slot-machine when tapped (note that I disabled user interaction on the pickers so they can only be spun using the button):
enum Suit: String {
case heart, club, spade, diamond
var displayImage: Image {
return Image(systemName: "suit.\(self.rawValue).fill")
}
}
struct ContentView: View {
#State private var suits: [Suit] = [.heart, .club, .spade, .diamond]
#State private var selectedSuits: [Suit] = [.heart, .heart, .heart, .heart]
var body: some View {
VStack {
HStack(spacing: 0) {
ForEach(0..<self.selectedSuits.count, id: \.self) { index in
Picker("Suits", selection: self.$selectedSuits[index]) {
ForEach(self.suits, id: \.self) { suit in
suit.displayImage
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.clipped()
.disabled(true)
}
}
Button(action: self.spin) {
Text("Spin")
}
}
}
private func spin() {
self.selectedSuits = self.selectedSuits.map { _ in
self.suits.randomElement()!
}
}
}
This is just an example, and could no doubt be improved, but it's a decent starting point.
Keep in mind that this code does throw a warning in Xcode Beta 5 -
'subscript(_:)' is deprecated: See Release Notes for migration path.
I haven't had a chance to look into this, but the example still works and should help you with what you're trying to achieve.