Extending View with Extra Function Without Using Anyview

How do you create a SwiftUI view that takes an optional secondary View argument?

November 2021 update (Works in Xcode 11.x, 12.x, and 13.x)

After some thought and a bit of trial and error, I figured it out. It seems a bit obvious in hindsight.

struct SomeCustomView<Content>: View where Content: View {
let title: String
let content: Content

init(title: String, @ViewBuilder content: @escaping () -> Content) {
self.title = title
self.content = content()
}

// returns a new View that includes the View defined in 'body'
func sideContent<SideContent: View>(@ViewBuilder side: @escaping () -> SideContent) -> some View {
HStack {
self // self is SomeCustomView
side()
}
}

var body: some View {
VStack {
Text(title)
content
}
}
}

It works with or without the method call.

SomeCustomView(title: "string argument") {
// some view
}

SomeCustomView(title: "hello") {
// some view
}.sideContent {
// another view
}

Previous code with subtle bug: body should be self

    func sideContent<SideContent: View>(@ViewBuilder side: @escaping () -> SideContent) -> some View {
HStack {
body // <--- subtle bug, updates to the main View are not propagated
side()
}
}

Thank you Jordan Smith for pointing this out a long time ago.

How to apply multiple modifier in a function to a given View in SwiftUI?

I would solve this task in reverse order - generic implementation for array and single use as array with one element, like:

extension SwiftUI.View {
public func border(edge: Edge, color: SwiftUI.Color, width: CGFloat = 1, cornerRadius: CGFloat = 0) -> some SwiftUI.View {
self.border(edges: [edge], color: color, width: width, cornerRadius: cornerRadius)
}

public func border(edges: [Edge], color: SwiftUI.Color, width: CGFloat = 1, cornerRadius: CGFloat = 0) -> some View {
self // generic implementation here
}
}

SwiftUI: Array Not Updating In All Views When Referencing The Same Observed Object

Ok I got it working. All I had to do was move the functions I had in my view to my viewModel. Im guessing to keep everything in the same scope. My guess is that everything in ContentView was it's own copy. I'm not 100% certain on that, but it works now.

SwiftUI: Global Overlay That Can Be Triggered From Any View

It's quite easy - and entertaining - to build a "toast" in SwiftUI!

Let's do it!

struct Toast<Presenting>: View where Presenting: View {

/// The binding that decides the appropriate drawing in the body.
@Binding var isShowing: Bool
/// The view that will be "presenting" this toast
let presenting: () -> Presenting
/// The text to show
let text: Text

var body: some View {

GeometryReader { geometry in

ZStack(alignment: .center) {

self.presenting()
.blur(radius: self.isShowing ? 1 : 0)

VStack {
self.text
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)

}

}

}

}

Explanation of the body:

  • GeometryReader gives us the preferred size of the superview , thus allowing the perfect sizing for our Toast.
  • ZStack stacks views on top of each other.
  • The logic is trivial: if the toast is not supposed to be seen (isShowing == false), then we render the presenting view. If the toast has to be presented (isShowing == true), then we render the presenting view with a little bit of blur - because we can - and we create our toast next.
  • The toast is just a VStack with a Text, with custom frame sizing, some design bells and whistles (colors and corner radius), and a default slide transition.

I added this method on View to make the Toast creation easier:

extension View {

func toast(isShowing: Binding<Bool>, text: Text) -> some View {
Toast(isShowing: isShowing,
presenting: { self },
text: text)
}

}

And a little demo on how to use it:

struct ContentView: View {

@State var showToast: Bool = false

var body: some View {
NavigationView {
List(0..<100) { item in
Text("\(item)")
}
.navigationBarTitle(Text("A List"), displayMode: .large)
.navigationBarItems(trailing: Button(action: {
withAnimation {
self.showToast.toggle()
}
}){
Text("Toggle toast")
})
}
.toast(isShowing: $showToast, text: Text("Hello toast!"))
}

}

I used a NavigationView to make sure the view fills the entire screen, so the Toast is sized and positioned correctly.

The withAnimation block ensures the Toast transition is applied.


How it looks:

Sample Image

It's easy to extend the Toast with the power of SwiftUI DSL.

The Text property can easily become a @ViewBuilder closure to accomodate the most extravagant of the layouts.


To add it to your content view:

struct ContentView : View {
@State private var liked: Bool = false

var body: some View {
VStack {
LikeButton(liked: $liked)
}
// make it bigger by using "frame" or wrapping it in "NavigationView"
.toast(isShowing: $liked, text: Text("Hello toast!"))
}
}

How to hide the toast afte 2 seconds (as requested):

Append this code after .transition(.slide) in the toast VStack.

.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
self.isShowing = false
}
}
}

Tested on Xcode 11.1

How can gave a View as default value in a View Function in SwiftUI?

The most possible way is having 2 function's. Like down codes:

PS: We cannot have the function which carries the default value for a generic type.

struct ContentView: View {

var body: some View {

viewFunction(content: Text("Hello, world!"))

viewFunction()
}
}


func viewFunction<Content: View>(content: Content) -> some View {

return content

}

func viewFunction() -> some View {

return Text("Hello, world!")

}

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()
}
}


Related Topics



Leave a reply



Submit