In swiftui text there is a function called hidden (). When I set this function, the text is invisible. So far everything is fine. Now I want it to be invisible only if a Bool value is set. Unfortunately, I can not pass a bool value.
Text("Bitte Password eingeben")
.foregroundColor(.white)
.padding(EdgeInsets(top: 50, leading: 5, bottom: 0, trailing: 0))
.cornerRadius(5)
.textContentType(.password)
.hidden()
You can return the .hidden() version of the view when some state is enabled like this (note that the hidden version and non hidden version are two different types which is why the type eraser is necessary -- some View means all exists from the function must return the same type that conforms to View):
struct ContentView: View {
#State var isHidden = false
var body: some View {
let text = Text("Bitte Password eingeben")
.foregroundColor(.white)
.padding(EdgeInsets(top: 50, leading: 5, bottom: 0, trailing: 0))
.cornerRadius(5)
.textContentType(.password)
return isHidden ? AnyView(text) : AnyView(text.hidden())
}
}
I have written a little extension on View, because I find it quite surprising that hidden() doesn't take a parameter:
extension View {
func isHidden(_ hidden: Bool) -> some View {
if hidden {
return self.hidden().eraseToAnyView()
} else {
return self.eraseToAnyView()
}
}
func eraseToAnyView() -> AnyView {
return AnyView(self)
}
}
Related
I am trying to change the state of a Toggle from outside the view. I've tried ObservableObject and EnvironmentObject, but the toggle is expecting a Binding (#State).
I need to execute a callback when the user taps the toggle
I need to change the state of the toggle programmatically w/o executing the callback.
I am using a shared model for this and other views, ideally I'd like to be able to use that for an 'enabled' Bool to take the place of the State var isOn.
This code does let me execute the callback via the extension, but I cannot figure out how to change the State variable isOn externally, and if I was able to, I'm guessing my callback would be executed, which I don't want to happen.
import SwiftUI
struct ControlView: View {
var title: String
var panel: Int
var callback: ()-> Void
#State public var isOn = false // toggle state
#EnvironmentObject var state: MainViewModel
//#ViewBuilder
var body: some View {
VStack() {
// -- Header
HStack() {
Text(" ")
Image(self.state.panelIcon(panel: panel)).resizable().frame(width: 13.0, height: 13.0)
Text(title)
Spacer()
}.padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
.background(Color(red: 0.9, green: 0.9, blue: 0.9))
// -- Switch
Toggle(isOn: $isOn.didSet { (state) in
// Activate ARC
callback()
}) {
Text("Enable ARC")
}.padding(EdgeInsets(top: 0, leading: 12, bottom: 10, trailing: 12))
}.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color(red: 0.8, green: 0.8, blue: 0.8), lineWidth: 1.25)
).background(Color.white)
}
}
extension Binding {
func didSet(execute: #escaping (Value) -> Void) -> Binding {
return Binding(
get: { self.wrappedValue },
set: {
self.wrappedValue = $0
execute($0)
}
)
}
}
You're on the right track by creating a custom Binding with a set function that performs your side effect. But instead of using a State, create a custom Binding that directly modifies the enabled property of your ObservableObject. Example:
import PlaygroundSupport
import SwiftUI
class MyModel: ObservableObject {
#Published var enabled: Bool = false
#Published var sideEffectCount: Int = 0
}
struct RootView: View {
#EnvironmentObject var model: MyModel
var body: some View {
List {
Text("Side effect count: \(model.sideEffectCount)")
Button("Set to false programmatically") {
model.enabled = false
}
Button("Set to true programmatically") {
model.enabled = true
}
Toggle("Toggle without side effect", isOn: $model.enabled)
Toggle("Toggle WITH side effect", isOn: Binding(
get: { model.enabled },
set: { newValue in
withAnimation {
if newValue {
model.sideEffectCount += 1
}
model.enabled = newValue
}
}
))
}
}
}
PlaygroundPage.current.setLiveView(
RootView()
.environmentObject(MyModel())
)
You can use onChange to trigger a side effect as the result of a value changing, such as a Binding. e.g.
.onChange(of:isOn) { [isOn] newValue in
if newValue {
model.sideEffectCount += 1
}
model.enabled = newValue
}
I have this peace of code that I have done to create a custom animated button
import SwiftUI
struct CustomLoginButtons<ButtonText: View>: View { // it can conforms to whatever I want as long it conforms to View protocol
let buttonAction: () -> Void //buttons returns that, just an empty closure
let buttonText: ButtonText // generics for button text whatever i want
#State private var pressed = false // variable that hold states of a button
init(buttonAction: #escaping () -> Void, #ViewBuilder buttonText: () -> ButtonText ) {
//the #escaping it means here that the action will be ignored here, for now, beacause it will be used somewhere else
self.buttonAction = buttonAction
self.buttonText = buttonText()// here its being initialized for building the button text
}
var body: some View {
Button(action: buttonAction) {// here i am passing the action closure to be executed
buttonText
.padding()
.frame(minWidth: 0, maxWidth: .infinity)/* infinity meas that the component will fit the entire view, that way
i can set the size in the view that i want to use this button*/
.background(Capsule().fill(Color.blue))
.foregroundColor(Color.white)
.overlay(RoundedRectangle(cornerRadius: 30).stroke(Color.white, lineWidth: 1.5))
.scaleEffect(self.pressed ? 1.2 : 1.0)
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
let impactHeavy = UIImpactFeedbackGenerator(style: .soft)
impactHeavy.impactOccurred()
withAnimation(.easeInOut(duration: 0.2)) {
self.pressed = pressing
}
}, perform: { })
}
}
}
my problem is currently when I tap on this button, the action is not performed, and I don't know why.
CustomLoginButtons(buttonAction: {
print("Tapped")
}) {
Text("Login")
}
that is how I am calling this button.
I need to use a Picker view but I don't see any options to hide the green focus border.
Code:
#State private var selectedIndex = 0
var values: [String] = (0 ... 12).map { String($0) }
var body: some View {
Picker(selection: $selectedIndex, label: Text("")) {
ForEach(0 ..< values.count) {
Text(values[$0])
}
}
.labelsHidden()
}
The following extension puts a black overlay over the picker border.
Result
Code
extension Picker {
func focusBorderHidden() -> some View {
let isWatchOS7: Bool = {
if #available(watchOS 7, *) {
return true
}
return false
}()
let padding: EdgeInsets = {
if isWatchOS7 {
return .init(top: 17, leading: 0, bottom: 0, trailing: 0)
}
return .init(top: 8.5, leading: 0.5, bottom: 8.5, trailing: 0.5)
}()
return self
.overlay(
RoundedRectangle(cornerRadius: isWatchOS7 ? 8 : 7)
.stroke(Color.black, lineWidth: isWatchOS7 ? 4 : 3.5)
.offset(y: isWatchOS7 ? 0 : 8)
.padding(padding)
)
}
}
Usage
Make sure .focusBorderHidden() is the first modifier.
Picker( [...] ) {
[...]
}
.focusBorderHidden()
[...]
On the Picker, something like this can be added to cover up the green border.
#ScaledMetric var borderWidth: CGFloat = 5 // or it can be 3
Picker {
...
}.border(Color.black, width: borderWidth)
Adding a mask of cornerRadius of whatever required but with a padding of 2 or over (so as to mask the outer edge of the view) as the green border width on the watch tends to be around 2... will do the trick
.mask(RoundedRectangle(cornerRadius: 12).padding(2))
I like the border method from Ray Hunter too but to keep things tidy and simple I rather stay away from having lots and lots of #... variables
How do I check to see if dark mode on the device is enabled. I want to check this from within a view and conditionally show or hide a shadow.
I thought I could jus get the colorScheme from the environment but I think I'm missing something.
struct FloatingAddButton : View {
#Environment(\.colorScheme) var colorScheme
#Binding var openAddModal: Bool
var body : some View {
VStack {
Spacer()
HStack() {
Spacer()
Button(action: {
self.openAddModal = true
}) {
ZStack {
Circle()
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
if(self.colorScheme == .light) {
.shadow(color: .secondary, radius: 5, x: 0, y: 0)
}
Image(systemName: "plus")
.foregroundColor(Color.white)
}
} // End Button
}
}
}
}
In my code, I have a simple View extension, that makes the code a lot more readable. With it, I can apply modifiers conditionally:
.conditionalModifier(self.colorScheme == .light, LightShadow())
The full implementation is below:
extension View {
// If condition is met, apply modifier, otherwise, leave the view untouched
public func conditionalModifier<T>(_ condition: Bool, _ modifier: T) -> some View where T: ViewModifier {
Group {
if condition {
self.modifier(modifier)
} else {
self
}
}
}
}
struct FloatingAddButton : View {
#Environment(\.colorScheme) var colorScheme
#Binding var openAddModal: Bool
var body : some View {
VStack {
Spacer()
HStack() {
Spacer()
Button(action: { self.openAddModal = true }) {
ZStack {
Circle()
.foregroundColor(Color(.red))
.frame(width: 50, height: 50, alignment: .center)
.conditionalModifier(self.colorScheme == .light, LightShadow())
Image(systemName: "plus")
.foregroundColor(Color.white)
}
}
} // End Button
}
}
}
struct LightShadow: ViewModifier {
func body(content: Content) -> some View {
content.shadow(color: .secondary, radius: 5, x: 0, y: 0)
}
}
If you ever have a case where you want to apply different modifiers for true and false, here's another extension:
extension View {
// Apply trueModifier if condition is met, or falseModifier if not.
public func conditionalModifier<M1, M2>(_ condition: Bool, _ trueModifier: M1, _ falseModifier: M2) -> some View where M1: ViewModifier, M2: ViewModifier {
Group {
if condition {
self.modifier(trueModifier)
} else {
self.modifier(falseModifier)
}
}
}
}
You are using colorScheme correctly. But it looks like you have a different issue - placing a modifier inside an if statement. I found that, unlike a View, modifiers don't work that way.
The answer is to create a custom ViewModifier. In your case I'd package everything up into one modifier like this:
struct CircleStyle: ViewModifier {
#Environment (\.colorScheme) var colorScheme:ColorScheme
func body(content: Content) -> some View {
if colorScheme == .light {
return content
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
.shadow(color: .secondary, radius: 5, x: 0, y: 0)
} else {
return content
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
}
}
And to use it:
Circle()..modifier(CircleStyle())
If you need to add more variables from your model, simply pass it into your modifier.
Thanks to #dfd for pointing out that I can't use an if statement with a modifier. I updated my code like this for now. This just returns different versions of the circle in light and dark mode.
if colorScheme == .light {
Circle()
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
.shadow(color: .secondary, radius: 5, x: 0, y: 0)
} else {
Circle()
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
}
SwiftUI
With the \.colorScheme key of an Environment variable:
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
Text(colorScheme == .dark ? "In dark mode" : "In light mode")
}
}
Also, it automatically updates on the change of the environment color scheme.
UIKit
To check the current, all object those conform to UITraitEnvironment protocol, including all UIView subclasses and all UIViewConttroller subclasses have access to the current style:
myUIView.traitCollection.userInterfaceStyle == .dark
myUIViewController.traitCollection.userInterfaceStyle == .dark
To detect the change of the style, here is the full detailed answer
SwiftUI makes it really simply to detect when dark mode is enabled. We simply have to add a #Enviroment variable and use .colorScheme property to scan the settings on our device and see if dark mode is enabled.
Let's take a look at the example below.
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
ZStack {
Color(colorScheme == .light ? .blue : .red)
Text("Hello, World!")
}
}
}
In the code above we are creating the #Environment variable to see if our device is in dark mode. Then inside of our body view we are setting the background color to red if its in dark mode or blue if its not in dark mode by using our colorScheme variable inside of a ternary operator.
A great use case for this is if you want to support different custom UI's for when the users device is in dark mode.
Happy Coding ;
Is there a way to remove separators or adjust separator insets in List view in SwiftUI?
In UIKit it can be achieved through
tableView.separatorStyle = .none
and
tableview.separatorInset = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18)
What are the corresponding SwiftUI alternatives?
For ios 13 not for ios 14
You can remove separaters using: UITableView.appearance().separatorStyle = .none in SwiftUI
Just add on
List() {
}.onAppear {
UITableView.appearance().separatorColor = .clear
}
or
struct SomeListView : View {
init( ) {
UITableView.appearance().separatorStyle = .none
}
var body : some View {
Text("TEST")
}
struct CallList : View {
var body : some View {
List() {
SomeListView()
}
}
}
iOS 15.0+
Mac Catalyst 15.0+
listRowSeparator(_:edges:)
Sets the display mode for the separator associated with this specific row.
https://developer.apple.com/
List {
ForEach(0..<10, id: \.self) { number in
Text("Text\(number)")
}.listRowSeparator(.hidden)
}
iOS 14.0+
struct ListRowSeperatorModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 15.0, *) {
content.listRowSeparator(.hidden)
} else {
content.onAppear {
UITableView.appearance().separatorStyle = .none
}
.onDisappear {
UITableView.appearance().separatorStyle = .singleLine
}
}
}
}
extension View {
func hideListRowSeparator() -> some View {
return self.modifier(ListRowSeperatorModifier())
}
}
Use .hideListRowSeparator() on ForEach.
List {
ForEach(0..<10, id: \.self) { number in
Text("Text\(number)")
}.hideListRowSeparator()
}
Searched so much to adjust insets for line separators in list. You do not need to do OnAppear(), just adjust the 'padding()' modifier for list.Simple!
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
To further tune the row inside list, you can use this -
Use this modifier:
.listRowInsets(EdgeInsets(....))
List {
Text("test")
.listRowInsets(EdgeInsets(top: -20, leading: -20, bottom: -20, trailing: -20))
}
This can all be done in SwiftUI
To remove separators in compare with the good old way in UIKit tableView.separatorStyle = .none, add this line in init or the table view's onAppear method:
init() {
UITableView.appearance().separatorStyle = .none
}
To adjust separator inset in compare with the line in UIKit tableview.separatorInset = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18), add this line in init or onAppear method:
List(...){
...
}.onAppear() {
UITableView.appearance().separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 50)
}
remove separator -> set clear color
init() {
UITableView.appearance().separatorColor = .clear
}
In SwiftUI:
Remove Separators
init() {
UITableView.appearance().separatorStyle = .none //remove separators
}
var body: some View {
List {
Text("Index 1")
Text("Index 2")
Text("Index 3")
Text("Index 4")
}
}
For the latter you can use listRowInsets:
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.listRowInsets(EdgeInsets(top: 0, left: 18, bottom: 0, right: 18))