How can I make a smoothed custom Picker on SwiftUI? - swift

I would like to replicate this picker in swiftUI. In particular, I have a button on the bottom left of the screen and when I click it I would like to show different icons (similar to the image below, but vertically). As soon as I click on one of the choices the button should shrink back to the initial form (circle) with the chosen icon.
When closed:
When open:
I am new to this language and to app in general, I tried with a Pop Up menu, but it is not the desired result, for now I have an horizontal segmented Picker.

You can't do this with the built-in Picker, because it doesn't offer a style like that and PickerStyle doesn't let you create custom styles (as of the 2022 releases).
You can create your own implementation out of other SwiftUI views instead. Here's what my brief attempt looks like:
Here's the code:
enum SoundOption {
case none
case alertsOnly
case all
}
struct SoundOptionPicker: View {
#Binding var option: SoundOption
#State private var isExpanded = false
var body: some View {
HStack(spacing: 0) {
button(for: .none, label: "volume.slash")
.foregroundColor(.red)
button(for: .alertsOnly, label: "speaker.badge.exclamationmark")
.foregroundColor(.white)
button(for: .all, label: "volume.2")
.foregroundColor(.white)
}
.buttonStyle(.plain)
.background {
Capsule(style: .continuous).foregroundColor(.black)
}
}
#ViewBuilder
private func button(for option: SoundOption, label: String) -> some View {
Button {
withAnimation(.easeOut) {
if isExpanded {
self.option = option
isExpanded = false
} else {
isExpanded = true
}
}
} label: {
Image(systemName: label)
.fontWeight(.bold)
.padding(10)
}
.frame(width: shouldShow(option) ? buttonSize : 0, height: buttonSize)
.opacity(shouldShow(option) ? 1 : 0)
.clipped()
}
private var buttonSize: CGFloat { 44 }
private func shouldShow(_ option: SoundOption) -> Bool {
return isExpanded || option == self.option
}
}
struct ContentView: View {
#State var option = SoundOption.none
var body: some View {
ZStack {
Color(hue: 0.6, saturation: 1, brightness: 0.2)
SoundOptionPicker(option: $option)
.shadow(color: .gray, radius: 3)
.frame(width: 200, alignment: .trailing)
}
}
}

Related

ZStack blocks animation SwiftUI

So my goal is to be able to show a custom view from time to time over a SwiftUI tabview, so I thought I would place them both in a ZStack like this
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation(Animation.linear(duration: 10)) {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
}
This works just fine, but when I try to use withAnimation() no animation gets triggered. How can I make the overlaying view, disappear with animation?
Use .animation modifier with container, like below, so container could animate removing view
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
show = false // << withAnimation not needed anymore
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
.animation(Animation.linear(duration: 10), value: show) // << here !!
So I found a solution and what I think is the cause of this. My hypothesis is that the animation modifier does not handle ZIndex IF it is not explicitly set.
One solution to this is to set ZIndex to the view that should be on the top to something higher than the other view. Like this:
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
.zIndex(.infinity) // <-- this here makes the animation work
}
}
}

How to to add Blur overlay on top of all views using .Blur as ViewModifier in SwiftUI

I have created a viewModifier which blurs a view when added.
Problem is when I add it on parent view of all the contents, all views are blurred differently.
I assume it's because it goes to all the contents and blur each of them individually instead of adding an overlay blur to the parent view.
I used Swift UI's native blur because UIBlurEffect has very limited configuration i.e. I can't adjust the blur intensity to a specific value.
Here it is when the modifier is added in the parent ZStack:
Here when I add it on background image only:
Adding it to background image looks good but I need it to be on top of all views. Here is my code:
Main View
import SwiftUI
struct BlurViewDemo: View {
#State private var isPressed = true
#State private var blurColor: BlurColor = .none
var body: some View {
ZStack {
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("blur-view-demo-image")
.resizable()
.frame(width: frame.size.width, height: frame.size.height)
// When added here only the background image is blurred.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
VStack(spacing: 30) {
// TITLE STYLE
Text(getSelectedBlurTitle())
.font(.system(size: 60))
.offset(y: -40)
.foregroundColor(
blurColor == .dark && isPressed ? .white : .black
)
// TOGGLE BLUR BUTTON
Button(action: {
isPressed.toggle()
}, label: {
Text(isPressed ? "Blur: On" : "Blur: Off")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color(#colorLiteral(red: 0.2, green: 0.4, blue: 0.4, alpha: 1)))
// DEFAULT BUTTON
Button(action: {
isPressed = true
blurColor = .none
}, label: {
Text("Default")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.gray)
// LIGHT BUTTON
Button(action: {
isPressed = true
blurColor = .light
}, label: {
Text("Light")
.foregroundColor(.black)
})
.frame(width: 80, height: 40)
.background(Color.white)
// DARK BUTTON
Button(action: {
isPressed = true
blurColor = .dark
}, label: {
Text("Dark")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.black)
} //: VSTACK
} //: ZSTACK
.edgesIgnoringSafeArea(.all)
// When added here, the buttons are not blurred properly.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
private func getSelectedBlurTitle() -> String {
guard isPressed else { return "Clear"}
switch blurColor {
case .none:
return "Default"
case .light:
return "Light"
case .dark:
return "Dark"
}
}
}
struct BlurViewDemo_Previews: PreviewProvider {
static var previews: some View {
BlurViewDemo()
}
}
View Modifier
public enum BlurColor {
case none
case light
case dark
}
import SwiftUI
struct BlurModifier: ViewModifier {
#Binding private var showBlur: Bool
#Binding private var blurColor: BlurColor
#State private var blurRadius: CGFloat = 14
public init(showBlur: Binding<Bool>, blurColor: Binding<BlurColor>) {
self._showBlur = showBlur
self._blurColor = blurColor
}
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: showBlur ? blurRadius : 0, opaque: true)
.animation(Animation.easeInOut(duration: 0.3))
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(showBlur ? true : false)
Rectangle()
.fill(showBlur ? getBlurColor() : Color.white.opacity(0.0001))
.edgesIgnoringSafeArea(.all)
}
}
private func getBlurColor() -> Color {
switch blurColor {
case .none:
return Color.white.opacity(0.5)
case .light:
return Color.white.opacity(0.6)
case .dark:
return Color.black.opacity(0.5)
}
}
}

Align views in Picker

How do I align the Color views in a straight line with the text to the side?
To look like so (text aligned leading):
█  red
█  green
█  blue
Or this (text aligned center):
█    red
█  green
█   blue
Current code:
struct ContentView: View {
#State private var colorName: Colors = .red
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
}
}
}
}
}
enum Colors: String, CaseIterable, Identifiable {
case red
case green
case blue
var id: String { rawValue }
var asColor: Color {
switch self {
case .red: return .red
case .green: return .green
case .blue: return .blue
}
}
}
Result (not aligned properly):
Without the Picker, I found it is possible to use alignmentGuide(_:computeValue:) to achieve the result. However, this needs to be in a Picker.
Attempt:
VStack(alignment: .custom) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
.alignmentGuide(.custom) { d in
d[.leading]
}
Text(color.rawValue)
.frame(maxWidth: .infinity)
.fixedSize()
}
}
.frame(height: 50)
}
/* ... */
extension HorizontalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[HorizontalAlignment.leading]
}
}
static let custom = HorizontalAlignment(CustomAlignment.self)
}
Result of attempt:
Possible solution is to use dynamic width for labels applied by max calculated one using view preferences.
Here is a demo. Tested with Xcode 13beta / iOS15
Note: the ViewWidthKey is taken from my other answer https://stackoverflow.com/a/63253241/12299030
struct ContentView: View {
#State private var colorName: Colors = .red
#State private var maxWidth = CGFloat.zero
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
}
.background(GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.frame(in: .local).size.width)
})
.onPreferenceChange(ViewWidthKey.self) {
self.maxWidth = max($0, maxWidth)
}
.frame(minWidth: maxWidth, alignment: .leading)
}
}
}
}
I think this should align your text and fix your issue.
struct ContentView: View {
#State private var colorName: Colors = .red
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack() {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
.frame(width: 100, height: 30, alignment: .leading)
}
}
}
}
}
A simple .frame modifier will fix these issues, try to frame your text together or use a Label if you don't want to complicate things when it comes to pickers or list views.
If you do go forward with this solution try to experiment with the width and height based on your requirements, and see if you want .leading, or .trailing in the alignment

Show/Hide Password - How can I add this feature?

I've looked through the forums but I'm seeing mixed answers especially ones from an old Xcode version.
I only decided to add this after already typing up the code I have in this:
How could I go about doing that? I was wanting the 'Eyeball' toggle implemented on the password field.
You can simply use this view instead of SecureField. It has the eye icon inside, so for most cases you don't need to care about anything.
struct SecureInputView: View {
#Binding private var text: String
#State private var isSecured: Bool = true
private var title: String
init(_ title: String, text: Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecured {
SecureField(title, text: $text)
} else {
TextField(title, text: $text)
}
}.padding(.trailing, 32)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
}
}
}
Copy paste this view into your app, and instead of SecureField just use SecureInputView.
Example: SecureInputView("Password", text: $viewModel.password)
The possible approach is to show either TextField or SecureField joined to one storage, like in below demo:
Updated: Xcode 13.4 / iOS 15.5
with FocusState, now it is possible to change fields without having the keyboard disappear
Main part:
if showPassword {
TextField("Placeholer", text: $password)
.focused($inFocus, equals: .plain)
} else {
SecureField("Placeholder", text: $password)
.focused($inFocus, equals: .secure)
}
Button("toggle") {
self.showPassword.toggle()
inFocus = showPassword ? .plain : .secure
}
Test module in project is here
Old:
struct DemoShowPassword: View {
#State private var showPassword: Bool = false
#State private var password = "demo"
var body: some View {
VStack {
if showPassword {
TextField("Placeholer", text: $password)
} else {
SecureField("Placeholder", text: $password)
}
Button("toggle") {
self.showPassword.toggle()
}
}
}
}
For those still looking for a simple solution to this issue (requires iOS 15 for swiftUI 3):
With the new #FocusState introduced in swiftUI 3, it's possible to keep focus and keyboard open while changing State.
By using the opacity modifier instead of conditionally changing between SecureField and TextField, the focus can jump between the two without issues with the keyboard.
This allows you to toggle between revealing and hiding the password with the the eye button included in the ZStack.
import SwiftUI
struct SecureTextFieldWithReveal: View {
#FocusState var focus1: Bool
#FocusState var focus2: Bool
#State var showPassword: Bool = false
#State var text: String = ""
var body: some View {
HStack {
ZStack(alignment: .trailing) {
TextField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus1)
.opacity(showPassword ? 1 : 0)
SecureField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus2)
.opacity(showPassword ? 0 : 1)
Button(action: {
showPassword.toggle()
if showPassword { focus1 = true } else { focus2 = true }
}, label: {
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill").font(.system(size: 16, weight: .regular))
.padding()
})
}
}
}
}
Password field hidden
Password field revealed
This is the code in LoginModifier:
import SwiftUI
struct LoginModifier: ViewModifier {
var borderColor: Color = Color.gray
func body(content: Content) -> some View {
content
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(borderColor, lineWidth: 1))
}
}
The only issue I've had with this method is that on regaining focus SecureField will automatically clear any text already entered if you start typing. This seems to be a design choice by Apple.
I am using this approach for now in my current application. I would like to say that it works flawlessly.
#ViewBuilder
func secureField() -> some View {
if self.showPassword {
TextField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
} else {
SecureField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
}
}
Use:
HStack{
Image(systemName: "lock.fill")
.foregroundColor(passwordText.isEmpty ? .secondary : .primary)
.font(.system(size: 18, weight: .medium, design: .default))
.frame(width: 18, height: 18, alignment: .center)
secureField()
if !passwordText.isEmpty {
Button(action: {
self.showPassword.toggle()
}, label: {
ZStack(alignment: .trailing){
Color.clear
.frame(maxWidth: 29, maxHeight: 60, alignment: .center)
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill")
.font(.system(size: 18, weight: .medium))
.foregroundColor(Color.init(red: 160.0/255.0, green: 160.0/255.0, blue: 160.0/255.0))
}
})
}
}
.padding(.horizontal, 15)
.background(Color.primary.opacity(0.05).cornerRadius(10))
.padding(.horizontal, 15)
I am afraid most answers here fail to mention that switching from SecureField to TextField reduces security. SecureField is essentially, per Apple documentation, simply a TextField where user input is masked [1]. However, SecureField also does one other job - it prevents using third-party keyboards (keyboard extensions) and thus protects user's security and privacy.
Ideal solution would be to have input field that is both "secure" and has mask()/unmask() methods. Unfortunately, the only advice I found is when you want to implement unmasking as other answers suggested, at least block third-party keyboards from your application entirely [2]:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
#main
struct MyApplication: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Should also mention that UIApplicationDelegate is part of UIKit, not SwiftUI. There is no "native" SwiftUI for the same purpose as for now, although the above works fine for now.
https://developer.apple.com/documentation/swiftui/securefield
https://www.securing.pl/en/third-party-iphone-keyboards-vs-your-ios-application-security/
For those that do not want the keyboard disappearing while typing:
struct CustomSecureField: View {
#State var password: String = ""
#State var isShowingPassword: Bool = false
var body: some View {
VStack{
ZStack{
HStack{
SecureField(
isShowingPassword ? "" : "Password",
text: $password) {
}.opacity(isShowingPassword ? 0 : 1)
// show only one of these is not empty.
if(!password.isEmpty){
Image(systemName: isShowingPassword ? "eye.slash" : "eye")
.foregroundColor(.white)
.frame(width: 20, height: 20, alignment: .center)
.modifier(TouchDownUpEventModifier(changeState: { (buttonState) in
if buttonState == .pressed {
isShowingPassword = true
} else {
isShowingPassword = false
}
}))
}
}
if(isShowingPassword){
HStack{
Text(password)
.foregroundColor(.white)
.allowsHitTesting(false)
Spacer()
}
}
}
}.padding(10)
.background(Color.gray)
}
}
and the on tap and release modifier:
public enum ButtonState {
case pressed
case notPressed
}
/// ViewModifier allows us to get a view, then modify it and return it
public struct TouchDownUpEventModifier: ViewModifier {
/// Properties marked with `#GestureState` automatically resets when the gesture ends/is cancelled
/// for example, once the finger lifts up, this will reset to false
/// this functionality is handled inside the `.updating` modifier
#GestureState private var isPressed = false
/// this is the closure that will get passed around.
/// we will update the ButtonState every time your finger touches down or up.
let changeState: (ButtonState) -> Void
/// a required function for ViewModifier.
/// content is the body content of the caller view
public func body(content: Content) -> some View {
/// declare the drag gesture
let drag = DragGesture(minimumDistance: 0)
/// this is called whenever the gesture is happening
/// because we do this on a `DragGesture`, this is called when the finger is down
.updating($isPressed) { (value, gestureState, transaction) in
/// setting the gestureState will automatically set `$isPressed`
gestureState = true
}
return content
.gesture(drag) /// add the gesture
.onChange(of: isPressed, perform: { (pressed) in /// call `changeState` whenever the state changes
/// `onChange` is available in iOS 14 and higher.
if pressed {
self.changeState(.pressed)
} else {
self.changeState(.notPressed)
}
})
}
/// if you're on iPad Swift Playgrounds and you put all of this code in a seperate file,
/// you need to add a public init so that the compiler detects it.
public init(changeState: #escaping (ButtonState) -> Void) {
self.changeState = changeState
}
}
From what I have seen there is no easy way to keep the text showing unless you want to lose focus on your text.
Cheers!
#Derwrecked's answer really gave me some good inspirations: instead using two TextField, change SecureField opacity and show/hide a Text can avoid keyboard dismissing problem, but in his answer that long TouchDownUpEventModifier seems unnecessarily complicated, you can easily achieve the same effect using a Button with label.
So below is my approach, and the previews look like this
import SwiftUI
struct SecureInput: View {
let placeholder: String
#State private var showText: Bool = false
#State var text: String
var onCommit: (()->Void)?
var body: some View {
HStack {
ZStack {
SecureField(placeholder, text: $text, onCommit: {
onCommit?()
})
.opacity(showText ? 0 : 1)
if showText {
HStack {
Text(text)
.lineLimit(1)
Spacer()
}
}
}
Button(action: {
showText.toggle()
}, label: {
Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
})
.accentColor(.secondary)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary, lineWidth: 1)
.foregroundColor(.clear))
}
}
struct SecureInput_Previews: PreviewProvider {
static var previews: some View {
Group {
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.previewLayout(.fixed(width: 400, height: 100))
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.preferredColorScheme(.dark)
.previewLayout(.fixed(width: 400, height: 100))
}
}
}
An known issue for this approach: since when password is shown, SecureField has 0.0 opacity, so input cursor is not visible. But users can still keep typing without losing keyboard focus, so I find it acceptable, if anyone has a solution for this, please comment and share.
I've been looking for a nice solution for my use-case. I had to have an indicator which field is in focus. Successfully done that with onEditingChanged from TextField, but SecureField doesn't provide that closure. I tried stacking them both and disabling the SecureField so it only shows 'hidden' characters. That resulted in cursor sticking to the TextField text while SecureField text had different text width which made it seem buggy. Imagine a password with a lot of I's in it. The idea is to have a main binding with two side bindings that update the main one and sync each other.
struct CustomSecureField : View {
var label : String
#Binding var text : String
#State var isEditing = false
#State var isHidden = true
var body : some View {
let showPasswordBinding = Binding<String> {
self.text
} set: {
self.text = $0
}
let hidePasswordBinding = Binding<String> {
String.init(repeating: "●", count: self.text.count)
} set: { newValue in
if(newValue.count < self.text.count) {
self.text = ""
} else {
self.text.append(contentsOf: newValue.suffix(newValue.count - self.text.count) )
}
}
return ZStack(alignment: .trailing) {
TextField(
label,
text: isHidden ? hidePasswordBinding : showPasswordBinding,
onEditingChanged: { editingChanged in
isEditing = editingChanged
}
)
Image("eye").frame(width: 50, height: 50).onTapGesture {
isHidden.toggle()
}
}
}
}
}
Crazy (AKA don't use in production) and very breakable solution here (but working at the time of writing):
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
if secure {
var secureField = self
withUnsafeMutablePointer(to: &secureField) { pointer in
let offset = 32
let valuePointer = UnsafeMutableRawPointer(mutating: pointer)
.assumingMemoryBound(to: Bool.self)
.advanced(by: offset)
valuePointer.pointee = true
}
return secureField
} else {
return self
}
}
}
Usage
#State securing = true
...
TextField(...)
.secure(securing)
#Vahagn Gevorgyan's answer was almost correct but some people were struggling with maintaining state... this is because the field is using a binding which should ideally be held in a parent view. Therefore just update the bindings to state variables like this
struct SecureInputView: View {
let placeholder: String
#State var text: String
#State var isSecure: Bool = true
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecure {
SecureField(placeholder, text: $text)
} else {
TextField(placeholder, text: $text)
}
}.padding(.trailing, 32)
Button {
isSecure.toggle()
} label: {
Image(systemName: isSecure ? "lock.fill" : "lock.open")
}
}
}
}
#State private var isPasswordVisible = false
ZStack {
TextField("", text: $password)
.opacity(isPasswordVisible ? 1 : 0)
SecureField("", text: $password)
.opacity(isPasswordVisible ? 0 : 1)
}
It doesn't need #Focus from iOS 15
Keyboard will not disappear/appear on changing isPasswordVisible
Password will not cleared on changing from visible to invisible then typing
Good Luck
I made a custom text field that combine SecureField and TextField.
This is an example where I used my custom field for both email and pwd.
This is my solution:
struct CustomTextField: View {
let imageName: String
let placeholderText: String
var isSecureInput: Bool = false ///< define if this text field is secured and require eye button
#State private var isSecured: Bool
#Binding var text: String
init(image: String,
placeholder: String,
text: Binding<String>,
isSecureInput: Bool) {
imageName = image
placeholderText = placeholder
self._text = text
self.isSecureInput = isSecureInput
isSecured = isSecureInput
}
var body: some View {
VStack {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
if isSecureInput {
Group {
if isSecured {
SecureField(placeholderText, text: $text)
}
else {
TextField(text, text: $text)
}
}
.disableAutocorrection(true)
.autocapitalization(.none)
.textContentType(.password)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
}
}
else {
TextField(placeholderText, text: $text)
}
}
Divider()
}
}
}

Show line / separator view in SwiftUI

I want to show a separator line in my SwiftUI app. To achieve that, I tried to create an empty view with a fixed frame and a background color / border:
EmptyView()
.frame(width: 200, height: 2)
.background(Color.black) // or:
.border(Color.black, width: 2)
Unfortunately, I cannot see any dark view showing up.
Is there a way to show a separator / line view?
Use Divider:
A visual element that can be used to separate other content.
Example:
struct ContentView : View {
var body: some View {
VStack {
Text("Hello World")
Divider()
Text("Hello Another World")
}
}
}
Output:
If anyone is interested a divider, text, divider, looking like this:
LabelledDivider code
struct LabelledDivider: View {
let label: String
let horizontalPadding: CGFloat
let color: Color
init(label: String, horizontalPadding: CGFloat = 20, color: Color = .gray) {
self.label = label
self.horizontalPadding = horizontalPadding
self.color = color
}
var body: some View {
HStack {
line
Text(label).foregroundColor(color)
line
}
}
var line: some View {
VStack { Divider().background(color) }.padding(horizontalPadding)
}
}
It's kind of ugly but I had to put the Dividers into a VStack to make them horizontal, otherwise, they will be vertical, due to HStack. Please let me know if you managed to simplify this :)
Also maybe using and stored properties for LabelledDivider might not be the most SwiftUI-y solution, so I'm open to improvements.
Example usage
This is the code that results in the screenshot seen above:
struct GetStartedView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SignInView()) {
Text("Sign In").buttonStyleEmerald()
}
LabelledDivider(label: "or")
NavigationLink(destination: SignUpView()) {
Text("Sign up").buttonStyleSaphire()
}
}.padding(20)
}
}
}
ButtonStyle
For sake of completness, I also include buttonStyle view modifiers:
struct ButtonStyle: ViewModifier {
private let color: Color
private let enabled: () -> Bool
init(color: Color, enabled: #escaping () -> Bool = { true }) {
self.color = color
self.enabled = enabled
}
dynamic func body(content: Content) -> some View {
content
.padding()
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.foregroundColor(Color.white)
.background(enabled() ? color : Color.black)
.cornerRadius(5)
}
}
extension View {
dynamic func buttonStyleEmerald(enabled: #escaping () -> Bool = { true }) -> some View {
ModifiedContent(content: self, modifier: ButtonStyle(color: Color.emerald, enabled: enabled))
}
dynamic func buttonStyleSaphire(enabled: #escaping () -> Bool = { true }) -> some View {
ModifiedContent(content: self, modifier: ButtonStyle(color: Color.saphire, enabled: enabled))
}
}
Edit: Please note that Color.saphire and Color.emerald are custom declared colors:
extension Color {
static var emerald: Color { .rgb(036, 180, 126) }
static var forest: Color { .rgb(062, 207, 142) }
}
extension Color {
static func rgb(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> Color {
func value(_ raw: UInt8) -> Double {
return Double(raw)/Double(255)
}
return Color(
red: value(red),
green: value(green),
blue: value(blue)
)
}
}
You can just draw a line by using Color. If you want to change the line width or padding, you can use frame or padding like other SwiftUI Components.
//Horizontal Line in VStack
VStack{
Color.gray.frame(height: 1 / UIScreen.main.scale)
}
//Vertical Line in HStack
HStack{
Color.gray.frame(width: 1 / UIScreen.main.scale)
}
If you are looking for a way to customize the divider, there isn't any. You must provide your custom implementation:
struct CustomDivider: View {
let height: CGFloat = 1
let color: Color = .white
let opacity: Double = 0.2
var body: some View {
Group {
Rectangle()
}
.frame(height: height)
.foregroundColor(color)
.opacity(opacity)
}
}
HStack {
VStack {
Divider()
}
Text("or")
.font(.caption)
.foregroundColor(Color(UIColor.systemGray))
VStack {
Divider()
}
}