SwiftUI - alternative to if let with a conditional closure
For such cases I prefer the following approach
struct PersonView: View {
@State private var age: Int? = 0
var body: some View {
VStack {
Text("Just a test")
AgeText
}
}
private var AgeText: some View {
if let age = self.age, age > 0 {
return Text("Display Age: \(age)")
} else {
return Text("Age must be greater than 0!")
}
}
}
SwiftUI if let inside View
Or you can create your own IfLet
view builder:
import SwiftUI
struct IfLet<Value, Content, NilContent>: View where Content: View, NilContent: View {
let value: Value?
let contentBuilder: (Value) -> Content
let nilContentBuilder: () -> NilContent
init(_ optionalValue: Value?, @ViewBuilder whenPresent contentBuilder: @escaping (Value) -> Content, @ViewBuilder whenNil nilContentBuilder: @escaping () -> NilContent) {
self.value = optionalValue
self.contentBuilder = contentBuilder
self.nilContentBuilder = nilContentBuilder
}
var body: some View {
Group {
if value != nil {
contentBuilder(value!)
} else {
nilContentBuilder()
}
}
}
}
extension IfLet where NilContent == EmptyView {
init(_ optionalValue: Value?, @ViewBuilder whenPresent contentBuilder: @escaping (Value) -> Content) {
self.init(optionalValue, whenPresent: contentBuilder, whenNil: { EmptyView() })
}
}
Using this, you can now do the following:
var body: some View {
IfLet(pieceOfData) { realData in
// realData is no longer optional
}
}
Want to respond if your optional is nil
?
var body: some View {
IfLet(pieceOfData, whenPresent: { realData in
// realData is no longer optional
DataView(realData)
}, whenNil: {
EmptyDataView()
})
}
How to treat if-let-else as a single view in SwiftUI?
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
Conditionally use view in SwiftUI
The simplest way to avoid using an extra container like HStack
is to annotate your body
property as @ViewBuilder
, like this:
@ViewBuilder
var body: some View {
if user.isLoggedIn {
MainView()
} else {
LoginView()
}
}
SwiftUI conditionally rendering one view or another
When you initialize a View (let's say A
) in the body of another View, what happens is that A is passed as an argument to some special functions generated by the compiler: this system of having implicit function calls in a context (in this case the body
of this View) is called "function builders", and they can be customized to have different behaviors. The one used in SwiftUI is ViewBuilder
: it "collects" all the Views that you make in the body and "merges" them in a single one (that's why the return type of body
is some View
).
ViewBuilder
contains some tricks to handle language constructs like if
statements by embedding logic like "show one view or the other" but, as of the current version of Swift (5.2), it doesn't support most other tools like 'if let, guard let, do catch'. Some of these will become available in the next Swift version.
One of the unsupported things is the ternary operator ?:
. In your example, the first line works because you're returning the same value for both the true
and the false
branches, but in the second line you're returning Views of different types, resulting in an error. Note that the same logic, used in a ViewBuilder context (Group
) works just fine:
Group {
if demoModel.isLoggedIn {
Text("Logged in")
} else {
LoginView()
}
}
And that's because ViewBuilder
knows how to manage simple if
statements.
SwiftUI when can i use if statement in result builder?
ToolbarContentBuilder does not allow if statements, it's designed to use ToolbarItemGroup
, where you can use if like a regular ViewBuilder:
var body: some View {
NavigationView {
Text("Hello, world!")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if foo { Button("foo") {} }
}
}
}
}
Declare a temporary variable or constant inside a closure that returns some View (SwiftUI)
This is due to a limitation where the Swift compiler only tries to infer a closure's return type if it is a single expression. Closure's that are processed by a result builder, such as @ViewBuilder
, are not subject to this limitation. Importantly, this limitation also doesn't affect functions (only closures).
I was able to make this work by moving the closure to a method inside the structure. Note: this is the same as @cluelessCoder's second solution, just excluding the @ViewBuilder
attribute.
struct GameView: View {
@State private var cards = [
Card(value: 100),
Card(value: 20),
Card(value: 80),
]
var body: some View {
MyListView(items: cards, content: cardView)
}
func cardView(for card: Card) -> some View {
let label = label(for: card) // only called once, and can be reused.
return Text(label)
}
func label(for card: Card) -> String {
return "Card with value \(card.value)"
}
}
Thanks to @cluelessCoder. I would have never stumbled upon this discovery without their input and helpful answer.
Related Topics
Count Unseen Messages with Firebase in Swift
Scrolling Slow on Mobile/iOS When Using Overflow:Scroll
Downloading Uiimage via Alamofireimage
If No Table View Results, Display "No Results" on Screen
Scale Image in an Uibutton to Aspectfit
Enable or Disable iPhone Push Notifications Inside the App
Xcode 7.1 Swift Framework App Builds But Not Archiving
How to Fix Failed to Fetch Default Token Error
Set a Maximum Number of Children in Firebase
iPhone 6 and 6 Plus Responsive Breakpoints
How to Handle iPhone Screen Sizes/Resolution for Background Images
Ask for User Permission to Receive Uilocalnotifications in iOS 8
iOS Convert Large Numbers to Smaller Format
How to Use Static Cells in Uitableview Without Using Storyboards
How to Change the Default Text of Cancel Button Which Appears in the Uisearchbar +Iphone