How to add SwiftUI custom style as a static extension - swift

In swiftui3 you can use buttonstyle shortcut like so
Button("0") {print("pressed 0")}
.buttonStyle(.bordered)
I would like to do that with my custom buttonstyle class
struct CrazyButtonStyle:ButtonStyle{
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.red)
}
}
calling it like this
Button("0") {print("pressed 0")}
.buttonStyle(.crazy)
I have tried
extension ButtonStyle{
static var crazy:CrazyButtonStyle {
get {
return CrazyButtonStyle()
}
}
}
but im getting this error
Contextual member reference to static property 'crazy' requires 'Self' constraint in the protocol extension

extension ButtonStyle where Self == CrazyButtonStyle{
static var crazy:CrazyButtonStyle {
get {
return CrazyButtonStyle()
}
}
}
adding where Self to the extension seems to work. But im not sure if this is the best way.

Apple's suggested way:
struct CrazyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.red)
}
}
extension ButtonStyle where Self == CrazyButtonStyle {
static var crazy: Self { Self() }
}
Usage:
Button(action: {}) {
Image(systemName: "forward.fill")
}
.buttonStyle(.crazy)
Source: Develop Apps for iOS Tutorial

You can define something like the following:
struct CrazyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Spacer()
configuration.label.foregroundColor(.red)
Spacer()
}
.scaleEffect(configuration.isPressed ? 0.90 : 1)
}
}
Then, to apply it to a Button:
Button("Crazy Button") {}
.buttonStyle(CrazyButtonStyle())
See swift docs for reference.
Also here are some other examples, with animations as well!

/// define your style
public struct MainButtonStyle: ButtonStyle {
public init(){}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(height: 50, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color(.primary).clipShape(RoundedRectangle(cornerRadius: 4)))
.foregroundColor(Color.white)
.scaleEffect(configuration.isPressed ? 0.95: 1)
}
}
/// create an enum of all new types you've just created
public enum ButtonStyles {
case main
}
/// add a new functionality to the view
public extension View {
#ViewBuilder
func buttonStyle(_ style: ButtonStyles) -> some View {
switch style {
case .main:
self.buttonStyle(MainButtonStyle())
}
}
}
then you can use it like this
Button { someAction() } label: {Text("COPY")}
.buttonStyle(.main)

Related

How to treat if-let-else as a single view in SwiftUI?

I'm trying to write an extension which adds a caption (I called it statusBar) right below it, no matter what type the caption is (e.g. Text, Image, Link...). So I tried the code below. But Xcode said an error at the line before .statusBar that Type '()' cannot conform to 'View'. You can find I added a comment in the code below.
I know there must be something wrong within my .statusBar, because the error disappeared when I replaced my if-let-else block with a single view (e.g. Text("Hello, world!")). But I still want to display different contents based on that if-let statement. So how can I do with my code to solve this?
// .statusBar extension
struct StatusBarView: ViewModifier {
let statusBar: AnyView
init<V: View>(statusBar: () -> V) {
self.statusBar = AnyView(statusBar())
}
func body(content: Content) -> some View {
VStack(spacing: 0) {
content
statusBar
}
}
}
extension View {
func statusBar<V: View>(statusBar: () -> V) -> some View {
self.modifier(StatusBarView(statusBar: statusBar))
}
}
// Inside main app view
Image(systemName: "link")
.font(.system(size: 48)) // Xcode error: Type '()' cannot conform to 'View'
.statusBar {
//
// If I change below if-let-else to a single view
// (e.g. Text("Hello, world!"))
// Then it works.
//
if let url = mediaManager.url {
Text(url.path)
} else {
Text("No media loaded.")
}
}
Make it closure argument a view builder, like
extension View {
func statusBar<V: View>(#ViewBuilder statusBar: () -> V) -> some View {
self.modifier(StatusBarView(statusBar: statusBar))
}
}
the same can be done in init of modifier, but not required specifically for this case of usage.
Tested with Xcode 13.4 / iOS 15.5
Wrap if-else into Group. Example:
Image(systemName: "link")
.font(.system(size: 48))
.statusBar {
Group {
if let url = mediaManager.url {
Text(url.path)
} else {
Text("No media loaded.")
}
}
}
You can also do like this:
Image(systemName: "link")
.font(.system(size: 48))
.statusBar {
ZStack {
if let url = mediaManager.url {
Text(url.path)
} else {
Text("No media loaded.")
}
}
}

How can I detect press gesture within ButtonStyle in SwiftUI? [duplicate]

I have a Button. I want to set custom background color for highlighted state. How can I do it in SwiftUI?
Button(action: signIn) {
Text("Sign In")
}
.padding(.all)
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
.font(Font.body.bold())
Updated for SwiftUI beta 5
SwiftUI does actually expose an API for this: ButtonStyle.
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(configuration.isPressed ? Color.red : Color.blue)
.cornerRadius(8.0)
}
}
// To use it
Button(action: {}) {
Text("Hello World")
}
.buttonStyle(MyButtonStyle())
As far as I can tell, theres no officially supported way to do this as of yet. Here is a little workaround that you can use. This produces the same behavior as in UIKit where tapping a button and dragging your finger off of it will keep the button highlighted.
struct HoverButton<Label: View>: View {
private let action: () -> ()
private let label: () -> Label
init(action: #escaping () -> (), label: #escaping () -> Label) {
self.action = action
self.label = label
}
#State private var pressed: Bool = false
var body: some View {
Button(action: action) {
label()
.foregroundColor(pressed ? .red : .blue)
.gesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in self.pressed = true }
.onEnded { _ in self.pressed = false })
}
}
}
I was looking for a similar functionality and I did it in the following way.
I created a special View struct returning a Button in the style I need, in this struct I added a State property selected. I have a variable named 'table' which is an Int since my buttons a round buttons with numbers on it
struct TableButton: View {
#State private var selected = false
var table: Int
var body: some View {
Button("\(table)") {
self.selected.toggle()
}
.frame(width: 50, height: 50)
.background(selected ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
Then I use in my content View the code
HStack(spacing: 10) {
ForEach((1...6), id: \.self) { table in
TableButton(table: table)
}
}
This creates an horizontal stack with 6 buttons which color blue when selected and red when deselected.
I am not a experienced developer but just tried all possible ways until I found that this is working for me, hopefully it is useful for others as well.
This is for the people who are not satisfied with the above solutions, as they raise other problems such as overlapping gestures(for example, it's quite hard to use this solution in scrollview now). Another crutch is to create a custom button style like this
struct CustomButtonStyle<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Self.Configuration) -> some View {
return change(configuration.isPressed)
}
}
So, we should just transfer the closure which will return the state of the button and create the button based on this parameter. It will be used like this:
struct CustomButton<Content>: View where Content: View {
var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
Button(action: { }, label: {
EmptyView()
})
.buttonStyle(CustomButtonStyle(change: { bool in
Text("\(bool ? "yo" : "yo2")")
}))
}
}
Okey let me clear everything again. Here is the exact solution
Create the below button modifier.
struct StateableButton<Content>: ButtonStyle where Content: View {
var change: (Bool) -> Content
func makeBody(configuration: Configuration) -> some View {
return change(configuration.isPressed)
}
}
Then use it like below one
Button(action: {
print("Do something")
}, label: {
// Don't create your button view in here
EmptyView()
})
.buttonStyle(StateableButton(change: { state in
// Create your button view in here
return HStack {
Image(systemName: "clock.arrow.circlepath")
Text(item)
Spacer()
Image(systemName: "arrow.up.backward")
}
.padding(.horizontal)
.frame(height: 50)
.background(state ? Color.black : Color.clear)
}))
You need to define a custom style that can be used to provide the two backgrounds for normal and highlighted states:
Button(action: {
print("action")
}, label: {
Text("My Button").padding()
})
.buttonStyle(HighlightableButtonStyle(normal: { Color.red },
highlighted: { Color.green }))
// Custom button style
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct HighlightableButtonStyle<N, H>: ButtonStyle where N: View, H: View {
private let alignment: Alignment
private let normal: () -> N
private let highlighted: () -> H
init(alignment: Alignment = .center, #ViewBuilder normal: #escaping () -> N, #ViewBuilder highlighted: #escaping () -> H) {
self.alignment = alignment
self.normal = normal
self.highlighted = highlighted
}
func makeBody(configuration: Configuration) -> some View {
return ZStack {
if configuration.isPressed {
configuration.label.background(alignment: alignment, content: normal)
}
else {
configuration.label.background(alignment: alignment, content: highlighted)
}
}
}
}

Appropriate extension for this use case

Originally I was looking to make an extension on Text for example:
extension Text {
var headerText: Text {
self
.bold()
.foregroundColor(.blue)
.padding() //<-- Doesn't work
}
}
and it all worked except for padding
So I had the bright idea of writing an extension on View instead but then the .bold() wouldn't work..
Looking for a more swifty way of doing this. Thanks
If I'm understanding correctly, this seems like the perfect case for a custom view modifier...
struct HeaderText: ViewModifier {
func body(content: Content) -> some View {
content
.bold()
.foregroundColor(.blue)
.padding()
}
}
...which you could then use like this:
struct ContentView: View {
var body: some View {
Text("This is a header")
.modifier(HeaderText())
}
}
You could also put the modifier inside a view extension to make it cleaner, like so:
extension View {
func headerText() -> ModifiedContent<Self, HeaderText> {
return modifier(HeaderText())
}
}
That would enable you to use it like this:
struct ContentView: View {
var body: some View {
Text("This is a header")
.headerText()
}
}

Using ternary operator in SwiftUI causes mismatching types error, why?

I currently have been using this Environment variable to adjust things for Dark and Light modes on my app.
#Environment(.colorScheme) var colorScheme
The problem I'm having is using that var to conditionally set a button style. This method works just fine.
if colorScheme == .dark {
Button("Create Account", action: {
}).buttonStyle(CinderDarkButtonStyle(geometry: geometry))
} else {
Button("Create Account", action: {
}).buttonStyle(CinderLightButtonStyle(geometry: geometry))
}
However doing it this way causes me to duplicate code all over the place on a relatively simple user interface. Whenever I attempt to do it this way I end up with errors stating that I have mismatched types.
Button("Create Account", action: {
//DO SOME ACTION
}).buttonStyle(
colorScheme == .dark ?
CinderDarkButtonStyle(geometry: geometry) :
CinderLightButtonStyle(geometry: geometry)
)
Besides what #Don said about the extra ), CinderDarkButtonStyle and CinderLightButtonStyle are different structs. They aren't the same type.
What you can do is make a custom function that returns an opaque type, as said in this answer. Try something like this:
struct ContentView: View {
#State var colorScheme = ColorScheme.dark
var body: some View {
GeometryReader { geometry in
Button("Create Account", action: {
//DO SOME ACTION
})
.buttonStyle(for: colorScheme, geometry: geometry) /// use custom buttonStyle function
}
}
}
extension Button {
#ViewBuilder
func buttonStyle(for colorScheme: ColorScheme, geometry: GeometryProxy) -> some View {
switch colorScheme {
case .light:
buttonStyle(CinderDarkButtonStyle(geometry: geometry))
case .dark:
buttonStyle(CinderLightButtonStyle(geometry: geometry))
}
}
}
The above switches between a buttonStyle(CinderDarkButtonStyle(geometry: geometry)) and buttonStyle(CinderLightButtonStyle(geometry: geometry)) modifier, based on the colorScheme.
You get the error because CinderDarkButtonStyle and CinderLightButtonStyle are, in fact, different types. And, ButtonStyle has an associated type, so you can't just to a straight cast to ButtonStyle.
If this were a normal View and not a ButtonStyle, you could use AnyView to do some type erasure so that the types would be equivalent.
In this case, it seems like it would be much cleaner just to move the logic about which button style to show inside the custom button style:
struct MyTestView : View {
var body: some View {
GeometryReader { geometry in
Button("Create Account", action: {
//DO SOME ACTION
})
.buttonStyle(CinderButtonStyle(geometry: geometry))
}
}
}
struct CinderButtonStyle : ButtonStyle {
var geometry : GeometryProxy
#Environment(\.colorScheme) private var colorScheme
#ViewBuilder func makeBody(configuration: Configuration) -> some View {
switch colorScheme {
case .dark:
darkBody(configuration: configuration)
case .light:
lightBody(configuration: configuration)
#unknown default:
lightBody(configuration: configuration)
}
}
#ViewBuilder func lightBody(configuration: Configuration) -> some View {
configuration.label //light modifications
}
#ViewBuilder func darkBody(configuration: Configuration) -> some View {
configuration.label //dark modifications
}
}
Update, using view modifiers:
struct CinderButtonStyle : ButtonStyle {
var geometry : GeometryProxy
#Environment(\.colorScheme) private var colorScheme
#ViewBuilder func makeBody(configuration: Configuration) -> some View {
switch colorScheme {
case .dark:
lightAndDarkModifications(configuration: configuration)
.modifier(DarkModifier())
default:
lightAndDarkModifications(configuration: configuration)
.modifier(LightModifier())
}
}
#ViewBuilder func lightAndDarkModifications(configuration: Configuration) -> some View {
configuration.label
}
}
struct DarkModifier : ViewModifier {
func body(content: Content) -> some View {
content
}
}
struct LightModifier : ViewModifier {
func body(content: Content) -> some View {
content
}
}

How can I know if a SwiftUI Button is enabled/disabled?

There is no isEnabled property for a SwiftUI button. How can i tell if it is enabled?
In regular UIKit, i would simply do
if button.isEnabeld == true {
} else {
}
but there is no SwiftUI equivalent.
Inside a view, if you wish to react to the state set by .disabled(true), you can use:
#Environment(\.isEnabled) var isEnabled
Since the environment can be used from within a View or a ViewModifier, this can be used to change layout properties of a view based on the state set from outside.
Unfortunately, ButtonStyle cannot directly use #Environment, but you can use a ViewModifier to inject environment values into a ButtonStyle in order to use the value from within a ButtonStyle:
// First create a button style that gets the isEnabled value injected
struct MyButtonStyle: ButtonStyle {
private let isEnabled: Bool
init(isEnabled: Bool = true) {
self.isEnabled = isEnabled
}
func makeBody(configuration: Configuration) -> some View {
return configuration
.label
.background(isEnabled ? .green : .gray)
.foregroundColor(isEnabled ? .black : .white)
}
}
// Then make a ViewModifier to inject the state
struct MyButtonModifier: ViewModifier {
#Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
return content.buttonStyle(MyButtonStyle(isEnabled: isEnabled))
}
}
// Then create a convenience function to apply the modifier
extension Button {
func styled() -> some View {
ModifiedContent(content: self, modifier: MyButtonModifier())
}
}
// Finally, try out the button and watch it respond to it's state
struct ContentView: View {
var body: some View {
Button("Test", {}).styled().disabled(true)
}
}
You can use this method to inject other things into a ButtonStyle, like size category and theme.
I use it with a custom style enum that contains all the flavours of button styles found in our design system.
From outside a view you should know if you used .disabled(true) modifier.
From inside a view you can use #Environment(\.isEnabled) to get that information:
struct MyButton: View {
let action: () -> Void
#Environment(\.isEnabled) private var isEnabled
var body: some View {
Button(action: action) {
Text("Click")
}
.foregroundColor(isEnabled ? .green : .gray)
}
}
struct MyButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
MyButton(action: {})
MyButton(action: {}).disabled(true)
}
}
}
The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.
In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some #Binding, #State, #EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.
Short answer: Just use inside struct:
#Environment(\.isEnabled) private var isEnabled
Button style with:
animation on hover change
animation on disable/enable change
can be applied on any button in native way of swiftUI
you need manually set size of buttons outside of the button
usage:
#State var isDisabled = false
///.......
Button("Styled button") { isDisabled.toggle() }
.buttonStyle(ButtStyle.BigButton()) // magic inside
.frame(width: 200, height: 50)
.disabled(isDisabled)
Button("switch isDisabled") { isDisabled.toggle() }
source code:
public struct ButtStyle { }
// Added style to easy stylyng in native way for SwiftUI
#available(macOS 11.0, *)
public extension ButtStyle {
struct BigButton: ButtonStyle {
init() {
}
public func makeBody(configuration: Configuration) -> some View {
BigButtonStyleView(configuration: configuration)
}
}
}
#available(macOS 11.0, *)
struct BigButtonStyleView : View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) var isEnabled // here we getting "disabled"
#State var hover : Bool = false
var body: some View {
// added animations
MainFrameMod()
.animation(.easeInOut(duration: 0.2), value: hover)
.animation(.easeInOut(duration: 0.2), value: isEnabled)
}
// added opacity on move hover change
// and disabled status
#ViewBuilder
func MainFrameMod() -> some View {
if isEnabled {
MainFrame()
.opacity(hover ? 1 : 0.8)
.onHover{ hover = $0 }
} else {
MainFrame()
.opacity(0.5)
}
}
// Main interface of button
func MainFrame() -> some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color(hex: 0xD8D8D8))
configuration.label
.foregroundColor(.black)
.font(.custom("SF Pro", size: 18))
}
}
}
As mentioned by other developers, the main idea of SwiftUI is that the UI remains synced with the data. You can perform this in many different ways. This includes #State, #EnvironmentObject, #Binding etc.
struct ContentView: View {
#State private var isEnabled: Bool = false
var body: some View {
VStack {
Button("Press me!") {
}.disabled(isEnabled)
}
.padding()
}
}