I would like to extend the SwiftUI.Button so that i can add a type prop and have that type change to different types of my theme buttons. But I am new to Swift and I can't figure out how to have my label and action props be generic.
In other words if i do this
import SwiftUI
struct Button: View {
var type: String = ""
var action = {}
var label = {
Text("Button")
}
var body: some View {
ZStack(content: {
SwiftUI.Button(action: action, label: label)
})
}
}
it limits the closure to only allow for label to return text()
how can this be done
also any suggestions on how i should to alter the "alterations" done to the button based on the type.
NOTE:
someone downvoted this because it is similar to button style query from another user however it is not.
that solution is for simply adding pre-made styles to the default SwiftUI.button struct and that's not my goal.
I'm attempting to extend SwiftUI.Button with a type property that can be passed and will set the styling based that input.
Although they share the same result they are not accomplishing the same goal. My solution will offer a dynamically styled component which can be used throughout the project. without the need of trailing .buttonStyle(BlueButtonStyle())
As I see you are trying re create apple Button, You can do like this, and then do your customisation in body:
struct Button<Content: View>: View {
let action: () -> Void
let label: () -> Content
init(action: #escaping () -> Void, #ViewBuilder label: #escaping () -> Content) {
self.action = action
self.label = label
}
init(action: #escaping () -> Void, title: String) where Content == Text {
self.init(action: action, label: { Text(title) })
}
var body: some View { label().onTapGesture { action() } }
}
use case:
Button(action: { print("hello") }, label: { Text("Button") })
Button(action: { print("hello") }, title: "Button")
The Conclusion of what swiftPunk put is the following.
struct Button<Content: View>: View {
let type: button_styles
let action: () -> Void
let label: () -> Content
enum button_styles {
case filled
case outlined
case plain
}
init(type: button_styles, action: #escaping () -> Void, #ViewBuilder label: #escaping () -> Content ) {
self.type = type
self.action = action
self.label = label
}
init(type: button_styles, action: #escaping () -> Void, title: String) where Content == Text {
self.init(type: type, action: action, label: { Text(title) })
}
init(action: #escaping () -> Void, title: String) where Content == Text {
self.init(type: .plain, action: action, label: { Text(title) })
}
init(action: #escaping () -> Void, #ViewBuilder label: #escaping () -> Content) {
self.init(type: .plain, action: action, label: label)
}
var body: some View {
switch type {
case .filled:
SwiftUI.Button(action: self.action, label: self.label).buttonStyle(FilledButtonStyle())
case .outlined:
SwiftUI.Button(action: self.action, label: self.label).buttonStyle(OutlinedButtonStyle())
case .plain:
SwiftUI.Button(action: self.action, label: self.label).buttonStyle(PlainButtonStyle())
}
}
}
struct FilledButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.headline)
.frame(maxWidth: 108, maxHeight: 34, alignment: .center)
.contentShape(Rectangle())
.foregroundColor(configuration.isPressed ? Color.white.opacity(0.5) : Color.white)
.background(configuration.isPressed ? Color("Red").opacity(0.5) : Color("Red"))
.cornerRadius(20)
}
}
struct OutlinedButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.headline)
.frame(maxWidth: 108, maxHeight: 34, alignment: .center)
.foregroundColor(Color("Grey"))
.background(Color.white.opacity(0))
.overlay(RoundedRectangle(cornerRadius:10).stroke(Color("Grey"), lineWidth: 2))
}
}
struct PlainButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.headline)
.frame(maxWidth: 108, maxHeight: 34, alignment: .center)
.contentShape(Rectangle())
.foregroundColor(configuration.isPressed ? Color.white.opacity(0.5) : Color("Grey"))
}
}
which will allow you to use the Button Struct like:
Button(type: .outlined, action: { print("pressed") }, title: "Button")
or
Button(action: { print("pressed") }, title: "Button")
or
Button(action: addItem, label: {
Label("Add Item", systemImage: "plus")
})
Related
Following codes gives an error of "Generic parameter 'Value' could not be inferred".
Appreciate anyone could point out the root cause and let me know how to fix it.
Thanks
import SwiftUI
private let readMe = """
This case study demonstrates how to enhance an existing SwiftUI component so that it can be driven \
off of optional and enum state.
The BottomMenuModifier component in this is file is primarily powered by a simple boolean binding, \
which means its content cannot be dynamic based off of the source of truth that drives its \
presentation, and it cannot make mutations to the source of truth.
However, by leveraging the binding transformations that come with this library we can extend the \
bottom menu component with additional APIs that allow presentation and dismissal to be powered by \
optionals and enums.
"""
struct ContentView: View {
#State var count: Int?
#State private var showPartialSheet = false
var body: some View {
Form {
Button("Show bottom menu") {
withAnimation {
self.count = 0
self.showPartialSheet = true
}
}
}
.bottomMenu($showPartialSheet, content: {
VStack {
Text("dfd")
}
})
.navigationTitle("Custom components")
}
}
private struct BottomMenuModifier<BottomMenuContent>: ViewModifier
where BottomMenuContent: View {
#Binding var isActive: Bool
let content: BottomMenuContent
init(isActive: Binding<Bool>, #ViewBuilder content: () -> BottomMenuContent) {
self._isActive = isActive
self.content = content()
}
func body(content: Content) -> some View {
content.overlay(
ZStack(alignment: .bottom) {
if self.isActive {
Rectangle()
.fill(Color.black.opacity(0.4))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
withAnimation {
self.isActive = false
}
}
.zIndex(1)
.transition(.opacity)
self.content
.padding()
.background(Color.white)
.cornerRadius(10)
.frame(maxWidth: .infinity)
.padding(24)
.padding(.bottom)
.zIndex(2)
.transition(.move(edge: .bottom))
}
}
.ignoresSafeArea()
)
}
}
extension View {
fileprivate func bottomMenu<Value, Content>(
_ showPartialSheet: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content
) -> some View
where Content: View {
self.modifier(
BottomMenuModifier(isActive: showPartialSheet, content: content)
)
}
}
struct CustomComponents_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Your bottomMenu function has an extra generic in its signature. Change it to:
fileprivate func bottomMenu<Content>(
(Note that Value from your original, which is unused, is removed)
I am new in SwiftUi and I am trying to show Alert message with TextField and action button, I want to use Alert function for this, but I can't add TextField in Alert function,I shared code at below, how can I add TextField in Alert function? thanks..
.alert(isPresented:$showAlertDialog) {
Alert(
title: Text("Please Enter sended E-mail Code"),
message: Text("There is no undo"),
primaryButton: .destructive(Text("Submit")) {
print("Deleting...")
},
secondaryButton: .cancel()
)
}
As of iOS 15 alerts are not capable of showing TextFields. But you can make a custom SwiftUI alert.
struct TextFieldAlert: ViewModifier {
#Binding var isPresented: Bool
let title: String
#Binding var text: String
let placeholder: String
let action: (String) -> Void
func body(content: Content) -> some View {
ZStack(alignment: .center) {
content
.disabled(isPresented)
if isPresented {
VStack {
Text(title).font(.headline).padding()
TextField(placeholder, text: $text).textFieldStyle(.roundedBorder).padding()
Divider()
HStack{
Spacer()
Button(role: .cancel) {
withAnimation {
isPresented.toggle()
}
} label: {
Text("Cancel")
}
Spacer()
Divider()
Spacer()
Button() {
action(text)
withAnimation {
isPresented.toggle()
}
} label: {
Text("Done")
}
Spacer()
}
}
.background(.background)
.frame(width: 300, height: 200)
.cornerRadius(20)
.overlay {
RoundedRectangle(cornerRadius: 20)
.stroke(.quaternary, lineWidth: 1)
}
}
}
}
}
extension View {
public func textFieldAlert(
isPresented: Binding<Bool>,
title: String,
text: Binding<String>,
placeholder: String = "",
action: #escaping (String) -> Void
) -> some View {
self.modifier(TextFieldAlert(isPresented: isPresented, title: title, text: text, placeholder: placeholder, action: action))
}
}
Use it on the root view. (don't attach it to Buttons to other inner elements for example). This will ensure that presenting view is disabled.
Sample usage:
struct ContentView: View {
#State var isAlertDisplayed = false
#State var text = "Text to change"
var body: some View {
VStack {
Text("Press the button to show the alert").multilineTextAlignment(.center)
Text("Current value is: \(text)")
Button("Change value") {
withAnimation {
isAlertDisplayed = true
}
}
.padding()
}
.textFieldAlert(isPresented: $isAlertDisplayed, title: "Some title", text: $text, placeholder: "Placeholder", action: { text in
print(text)
})
.padding()
}
}
I have a reset button that asks for confirmation first. I would like to set isSure to false is the user touches outside the component.
Can I do this from the Button component?
Here is my button:
struct ResetButton: View {
var onConfirmPress: () -> Void;
#State private var isSure: Bool = false;
var body: some View {
Button(action: {
if (self.isSure) {
self.onConfirmPress();
self.isSure.toggle();
} else {
self.isSure.toggle();
}
}) {
Text(self.isSure ? "Are you sure?" : "Reset")
}
}
}
here is one way to do it:
struct ContentView: View {
var onConfirmPress: () -> Void
#State private var isSure: Bool = false
var body: some View {
GeometryReader { geometry in
ZStack {
// a transparent rectangle under everything
Rectangle()
.frame(width: geometry.size.width, height: geometry.size.height)
.opacity(0.001) // <--- important
.layoutPriority(-1)
.onTapGesture {
self.isSure = false
print("---> onTapGesture self.isSure : \(self.isSure)")
}
Button(action: {
if (self.isSure) {
self.onConfirmPress()
}
self.isSure.toggle()
}) {
Text(self.isSure ? "Are you sure?" : "Reset").padding(10).border(Color.black)
}
}
}
}
}
Basically, we have some view, and we want a tap on its background to do something - meaning, we want to add a huge background that registers a tap. Note that .background is only offered the size of the main view, but can always set an explicit different size! If you know your size that's great, otherwise UIScreen could work...
This is hacky but seems to work!
extension View {
#ViewBuilder
private func onTapBackgroundContent(enabled: Bool, _ action: #escaping () -> Void) -> some View {
if enabled {
Color.clear
.frame(width: UIScreen.main.bounds.width * 2, height: UIScreen.main.bounds.height * 2)
.contentShape(Rectangle())
.onTapGesture(perform: action)
}
}
func onTapBackground(enabled: Bool, _ action: #escaping () -> Void) -> some View {
background(
onTapBackgroundContent(enabled: enabled, action)
)
}
}
Usage:
SomeView()
.onTapBackground(enabled: isShowingAlert) {
isShowingAlert = false
}
This can be easily changed to take a binding:
func onTapBackground(set value: Binding<Bool>) -> some View {
background(
onTapBackgroundContent(enabled: value.wrappedValue) { value.wrappedValue = false }
)
}
// later...
SomeView()
.onTapBackground(set: $isShowingAlert)
I need to create an alert with 3 buttons, but it looks like SwiftUI only gives us two options right now: one button or two buttons. I know that with UIKit, 3 buttons are achievable, but I can't seem to find a workaround in the latest version of SwiftUI to do this. Below is my code where I'm using only a primary and secondary button.
Button(action: {
self.showAlert = true
}){
Text("press me")
}
.alert(isPresented: self.$showAlert){
Alert(title: Text("select option"), message: Text("pls help"), primaryButton: Alert.Button.default(Text("yes"), action: {
print("yes clicked")
}), secondaryButton: Alert.Button.cancel(Text("no"), action: {
print("no clicked")
})
)
}
This is now possible on iOS 15/macOS 12 with a new version of the alert modifier: alert(_:isPresented:presenting:actions:).
It works a bit differently because the Alert struct isn't used anymore; you use regular SwiftUI Buttons instead. Add a ButtonRole to indicate which buttons perform the "cancel" action or "destructive" actions. Add the .keyboardShortcut(.defaultAction) modifier to a button to indicate it performs the principal action.
For example:
MyView()
.alert("Test", isPresented: $presentingAlert) {
Button("one", action: {})
Button("two", action: {}).keyboardShortcut(.defaultAction)
Button("three", role: .destructive, action: {})
Button("four", role: .cancel, action: {})
}
Creates the following alert:
.actionSheet, .sheet, and .popover are options to provide custom alerts. Consider this sample:
import SwiftUI
struct ContentView: View {
#State private var showModal = false
#State private var cnt = 0
var body: some View {
VStack {
Text("Counter is \(cnt)")
Button("Show alert") {
self.showModal = true
}
}
.sheet(isPresented: $showModal,
onDismiss: {
print(self.showModal)}) {
CustomAlert(message: "This is Modal view",
titlesAndActions: [("OK", {}),
("Increase", { self.cnt += 1 }),
("Cancel", nil)])
}
}
}
struct CustomAlert: View {
#Environment(\.presentationMode) var presentation
let message: String
let titlesAndActions: [(title: String, action: (() -> Void)?)] // = [.default(Text("OK"))]
var body: some View {
VStack {
Text(message)
Divider().padding([.leading, .trailing], 40)
HStack {
ForEach(titlesAndActions.indices, id: \.self) { i in
Button(self.titlesAndActions[i].title) {
(self.titlesAndActions[i].action ?? {})()
self.presentation.wrappedValue.dismiss()
}
.padding()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to be compatible with previous versions of iOS 15, consider to use actionSheet like this:
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Title"), message: Text("Choose one of this three:"), buttons: [
.default(Text("First")) { },
.default(Text("Second")) { },
.default(Text("Third")) { },
.cancel()
])
}
example below requires iOS 14.0 and available for both iPad and iPhone.
i also beleive this code will require minimum modifications to support iOS 13.0 as well.
(xcode project with usage example is available here)
import SwiftUI
public extension View {
func alert(if condition: Binding<Bool>,
title: String = "",
text: String = "",
buttons: [Alert.Option] = [.cancel("OK")]) -> some View {
let alert: AlertView = .init(condition, title, text, buttons)
return overlay(condition.wrappedValue ? alert : nil)
}
}
private struct AlertView : View {
#Binding var active: Bool
private let title: String
private let text: String
private let buttons: [Alert.Option]
private let cancel: Alert.Option?
init(_ active: Binding<Bool>, _ title: String, _ text: String, _ buttons: [Alert.Option]) {
self._active = active
self.title = title
self.text = text
self.buttons = buttons.filter { $0.role != .cancel }
self.cancel = buttons.first { $0.role == .cancel } ?? (buttons.isEmpty ? .cancel("OK") : nil)
}
var body: some View {
VStack() {
if title.count > 0 { Text(title).fontWeight(.semibold).padding([.top, .bottom], 5) }
if text.count > 0 { Text(text).fixedSize(horizontal: false, vertical: true) }
ForEach(buttons, id: \.label) { Divider(); button(for: $0) }
if let cancel { Divider(); button(for: cancel) }
}.padding()
.frame(maxWidth: width)
.background(Color(.secondarySystemBackground))
.cornerRadius(20)
.shadow(color: Color(.black), radius: 5)
.screenCover()
.onTapGesture { if cancellable { active = false } }
}
private func button(for option: Alert.Option) -> some View {
HStack {
Spacer()
Text(option.label)
.foregroundColor(option.role == .destructive ? Color(.red) : Color(.link))
.fontWeight(option.role == .cancel ? .semibold : .regular)
.padding([.top, .bottom], 5)
Spacer()
}.background(Color(.secondarySystemBackground))
.onTapGesture { if option.role != .cancel { option.action() }; active = false }
}
private var width: CGFloat { min((UIScreen.main.bounds.width * 0.8), 500) }
private var cancellable: Bool { cancel != nil }
}
extension Alert {
public final class Option {
public let label: String
public let role: Role
public let action: () -> Void
public init(_ label: String, role: Role = .regular, action: #escaping () -> Void = { }) {
self.label = label
self.role = role
self.action = action
}
public static func cancel(_ label: String) -> Option { .init(label, role: .cancel) }
}
}
public extension SwiftUI.Alert.Option { enum Role { case regular; case cancel; case destructive } }
public extension View {
func screenCover(_ color: Color = .init(UIColor.systemBackground), opacity: Double = 0.5) -> some View {
color.opacity(opacity)
.ignoresSafeArea()
.overlay(self)
}
}
SwiftUI layout is very different from what we are used to. Currently I'm fighting against TextFields. Specifically their touchable Area.
TextField(
.constant(""),
placeholder: Text("My text field")
)
.padding([.leading, .trailing])
.font(.body)
This results in a very small TextField (height wise)
Adding the frame modifier fixes the issue (visually)
TextField(
.constant(""),
placeholder: Text("My text field")
).frame(height: 60)
.padding([.leading, .trailing])
.font(.body)
but the touchable area remains the same.
I'm aware of the fact that the frame modifier does nothing else other than wrap the textField in another View with the specified height.
Is there any equivalent to resizable() for Image that will allow a taller TextField with wider touchable Area?
This solution only requires a #FocusState and an onTapGesture, and allows the user to tap anywhere, including the padded area, to focus the field. Tested with iOS 15.
struct MyView: View {
#Binding var text: String
#FocusState private var isFocused: Bool
var body: some View {
TextField("", text: $text)
.padding()
.background(Color.gray)
.focused($isFocused)
.onTapGesture {
isFocused = true
}
}
}
Bonus:
If you find yourself doing this on several text fields, making a custom TextFieldStyle will make things easier:
struct TappableTextFieldStyle: TextFieldStyle {
#FocusState private var textFieldFocused: Bool
func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding()
.focused($textFieldFocused)
.onTapGesture {
textFieldFocused = true
}
}
}
Then apply it to your text fields with:
TextField("", text: $text)
.textFieldStyle(TappableTextFieldStyle())
Solution with Button
If you don't mind using Introspect you can do it by saving the UITextField and calling becomeFirstResponder() on button press.
extension View {
public func textFieldFocusableArea() -> some View {
TextFieldButton { self.contentShape(Rectangle()) }
}
}
fileprivate struct TextFieldButton<Label: View>: View {
init(label: #escaping () -> Label) {
self.label = label
}
var label: () -> Label
private var textField = Weak<UITextField>(nil)
var body: some View {
Button(action: {
self.textField.value?.becomeFirstResponder()
}, label: {
label().introspectTextField {
self.textField.value = $0
}
}).buttonStyle(PlainButtonStyle())
}
}
/// Holds a weak reference to a value
public class Weak<T: AnyObject> {
public weak var value: T?
public init(_ value: T?) {
self.value = value
}
}
Example usage:
TextField(...)
.padding(100)
.textFieldFocusableArea()
Since I use this myself as well, I will keep it updated on github: https://gist.github.com/Amzd/d7d0c7de8eae8a771cb0ae3b99eab73d
New solution using ResponderChain
The Button solution will add styling and animation which might not be wanted therefore I now use a new method using my ResponderChain package
import ResponderChain
extension View {
public func textFieldFocusableArea() -> some View {
self.modifier(TextFieldFocusableAreaModifier())
}
}
fileprivate struct TextFieldFocusableAreaModifier: ViewModifier {
#EnvironmentObject private var chain: ResponderChain
#State private var id = UUID()
func body(content: Content) -> some View {
content
.contentShape(Rectangle())
.responderTag(id)
.onTapGesture {
chain.firstResponder = id
}
}
}
You'll have to set the ResponderChain as environment object in the SceneDelegate, check the README of ResponderChain for more info.
Solution Without Any 3rd Parties
Increasing the tappable area can be done without third parties:
Step1: Create a modified TextField. This is done so we can define the padding of our new TextField:
Code used from - https://stackoverflow.com/a/27066764/2217750
class ModifiedTextField: UITextField {
let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)
override open func textRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
}
Step 2: Make the new ModifiedTexField UIViewRepresentable so we can use it SwiftUI:
struct EnhancedTextField: UIViewRepresentable {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIView(context: Context) -> ModifiedTextField {
let textField = ModifiedTextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: ModifiedTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
let parent: EnhancedTextField
init(_ parent: EnhancedTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
Step3: Use the new EnhancedTextField wherever needed:
EnhancedTextField(placeholder: placeholder, text: $binding)
Note: To increase or decrease the tappable area just change the padding in ModifiedTextField
let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)
A little work around but works.
struct CustomTextField: View {
#State var name = ""
#State var isFocused = false
let textFieldsize : CGFloat = 20
var textFieldTouchAbleHeight : CGFloat = 200
var body: some View {
ZStack {
HStack{
Text(name)
.font(.system(size: textFieldsize))
.lineLimit(1)
.foregroundColor(isFocused ? Color.clear : Color.black)
.disabled(true)
Spacer()
}
.frame(alignment: .leading)
TextField(name, text: $name , onEditingChanged: { editingChanged in
isFocused = editingChanged
})
.font(.system(size: isFocused ? textFieldsize : textFieldTouchAbleHeight ))
.foregroundColor(isFocused ? Color.black : Color.clear)
.frame( height: isFocused ? 50 : textFieldTouchAbleHeight , alignment: .leading)
}.frame(width: 300, height: textFieldTouchAbleHeight + 10,alignment: .leading)
.disableAutocorrection(true)
.background(Color.white)
.padding(.horizontal,10)
.padding(.vertical,10)
.border(Color.red, width: 2)
}
}
I don't know which is better for you.
so, I post two solution.
1) If you want to shrink only input area.
var body: some View {
Form {
HStack {
Spacer().frame(width: 30)
TextField("input text", text: $inputText)
Spacer().frame(width: 30)
}
}
}
2) shrink a whole form area
var body: some View {
HStack {
Spacer().frame(width: 30)
Form {
TextField("input text", text: $restrictInput.text)
}
Spacer().frame(width: 30)
}
}
iOS 15 Solution with TextFieldStyle and additional header (it can be removed if need)
extension TextField {
func customStyle(_ title: String) -> some View {
self.textFieldStyle(CustomTextFieldStyle(title))
}
}
extension SecureField {
func customStyle(_ title: String, error) -> some View {
self.textFieldStyle(CustomTextFieldStyle(title))
}
}
struct CustomTextFieldStyle : TextFieldStyle {
#FocusState var focused: Bool
let title: String
init(_ title: String) {
self.title = title
}
public func _body(configuration: TextField<Self._Label>) -> some View {
VStack(alignment: .leading) {
Text(title)
.padding(.horizontal, 12)
configuration
.focused($focused)
.frame(height: 48)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.foregroundColor(.gray)
)
}.onTapGesture {
focused = true
}
}
}
Try using an overlay with a spacer to create a larger tapable/touchable area.
Create a myText variable:
#State private var myText = ""
Then, create your TextField with the following example formatting with an overlay:
TextField("Enter myText...", text: $myText)
.padding()
.frame(maxWidth: .infinity)
.padding(.horizontal)
.shadow(color: Color(.gray), radius: 3, x: 3, y: 3)
.overlay(
HStack {
Spacer()
})
Hope this works for you!
quick workaround would be to just put TextField in a button, and it'll make keyboard open no matter where you tap (in button); I know it's not a solution but it gets the job done (sort of).