How to treat if-let-else as a single view in SwiftUI? - swift

I'm trying to write an extension which adds a caption (I called it statusBar) right below it, no matter what type the caption is (e.g. Text, Image, Link...). So I tried the code below. But Xcode said an error at the line before .statusBar that Type '()' cannot conform to 'View'. You can find I added a comment in the code below.
I know there must be something wrong within my .statusBar, because the error disappeared when I replaced my if-let-else block with a single view (e.g. Text("Hello, world!")). But I still want to display different contents based on that if-let statement. So how can I do with my code to solve this?
// .statusBar extension
struct StatusBarView: ViewModifier {
let statusBar: AnyView
init<V: View>(statusBar: () -> V) {
self.statusBar = AnyView(statusBar())
}
func body(content: Content) -> some View {
VStack(spacing: 0) {
content
statusBar
}
}
}
extension View {
func statusBar<V: View>(statusBar: () -> V) -> some View {
self.modifier(StatusBarView(statusBar: statusBar))
}
}
// Inside main app view
Image(systemName: "link")
.font(.system(size: 48)) // Xcode error: Type '()' cannot conform to 'View'
.statusBar {
//
// If I change below if-let-else to a single view
// (e.g. Text("Hello, world!"))
// Then it works.
//
if let url = mediaManager.url {
Text(url.path)
} else {
Text("No media loaded.")
}
}

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

Wrap if-else into Group. Example:
Image(systemName: "link")
.font(.system(size: 48))
.statusBar {
Group {
if let url = mediaManager.url {
Text(url.path)
} else {
Text("No media loaded.")
}
}
}

You can also do like this:
Image(systemName: "link")
.font(.system(size: 48))
.statusBar {
ZStack {
if let url = mediaManager.url {
Text(url.path)
} else {
Text("No media loaded.")
}
}
}

Related

SwiftUI ViewModifier not working as a NavigationLink

I have the following ViewModifier and it does not work:
import SwiftUI
struct NavLink: ViewModifier {
let title: String
func body(content: Content) -> some View {
NavigationLink(destination: content) {
Text(title)
}
}
}
extension View {
func navLink(title: String) -> some View {
modifier(NavLink(title: title))
}
}
If I use that as follows:
import SwiftUI
struct MainScreen: View {
var body: some View {
NavigationStack() {
VStack {
// This does not work
NavStackScreen()
.navLink(title: "Nav Stack")
// But this does
Helper.linked(to: NavStackScreen(), title: "Nav Stack 2")
}
}
}
}
struct Helper {
static func linked(to destination: some View, title: String) -> some View {
NavigationLink(destination: destination) {
Text(title)
}
}
}
It creates the link and pushes a new view onto the screen if tapped; however, the contents of the NavStackScreen are not displayed, only an empty screen.
Any ideas about what is going on?
Contents of NavStackScreen for reference:
struct NavStackScreen: View {
var body: some View {
Text("Nav Stack Screen")
.font(.title)
.navigationTitle("Navigation Stack")
}
}
If I use a static helper function within a helper struct, then it works correctly:
static func linked(to destination: some View, title: String) -> some View {
NavigationLink(destination: destination) {
Text(title)
}
}
I added the full code of the MainView for reference and updated the example with more detail for easy reproduction.
The modifier doesn't work because the content argument is not the actual view being modified, but instead is a proxy:
content is a proxy for the view that will have the modifier represented by Self applied to it.
Reference.
This is what a quick debugging over the modifier shows:
(lldb) po content
SwiftUI._ViewModifier_Content<SwiftUIApp.NavLink>()
As the proxy is an internal type of SwiftUI, we can't know for sure why NavigationLink doesn't work with it.
A workaround would be to skip the modifier, and only add the extension over View:
extension View {
func navLink(title: String) -> some View {
NavigationLink(destination: content) {
Text(title)
}
}
}

SwiftUI extension generic where clause not matching

I have this simple ThemedNavigationButton view that handles some stuff whilst creating a NavigationLink (The inner workings aren't important):
struct ThemedNavigationButton<Destination, L>: View where Destination: View, L: View {
var destination: () -> Destination
var label: () -> L
var body: some View {
...
}
}
I use L here and not Label because I need to use the SwiftUI Label
next
which I use like this:
ThemedNavigationButton {
NextView()
} label: {
Label {
Text("Some text")
} icon: {
Image(systemName: "check")
.foregroundColor(theme.tint)
}
}
I want to create a simpler initialiser when it is used in this manner, so I came up with this:
extension ThemedNavigationButton where L == Label<Text, Image> {
init(text: String, systemImage: String, destination: #escaping () -> Destination) {
self.destination = destination
self.label = {
Label {
Text(text + text)
} icon: {
Image(systemName: systemImage)
}
}
}
}
which works great like this:
ThemedNavigationButton(text: "Some text", systemImage: "check") { NextView() }
The problem I have, is as soon as I add the image tint colour to the new initialiser I get the error:
Cannot convert value of type 'some View' to closure result type
'Image'
I'm guessing because my Image is no longer an Image. But what is it and how do I declare it. I can't use some View which is what the compiler is telling me it is.
Generics specialisation requires concrete types, so here is a possible approach to resolve this situation - introduce custom wrapper/proxy type and use it in extension.
Tested with Xcode 13.2
struct MyLabel: View { // new wrapper type
let text: String
let systemImage: String
var tintColor = Color.green
var body: some View {
Label {
Text(text + text)
} icon: {
Image(systemName: systemImage)
.foregroundColor(tintColor)
}
}
}
extension ThemedNavigationButton where L == MyLabel { // << here !!
init(text: String, systemImage: String, destination: #escaping () -> Destination) {
self.destination = destination
self.label = {
MyLabel(text: text, systemImage: systemImage)
}
}
}
To use the following notation:
ThemedNavigationButton(text: "Some text", systemImage: "check") { NextView() }
you can create a View with just one generic type for Destination, because the Label will receive basic String types.
You can set ThemedNavigationButton as follows:
// Only one generic type needed
struct ThemedNavigationButton<Destination: View>: View {
// Constants for the label (make them appear before "destination")
let text: String
let systemImage: String
// Destination view
var destination: () -> Destination
var body: some View {
// Show the views the way you want
VStack {
destination()
// Use the label this way
Label {
Text(text)
} icon: {
Image(systemName: systemImage)
}
}
}
}
Customise the body the way you need.
You can use it calling:
ThemedNavigationButton(text: "Some text", systemImage: "check") { NextView() }

Generic enum Menu SwiftUI

I'm trying to create a View struct in SwiftUI for a menu view like the code below. I'm getting an error though with "Failed to produce diagnostic for expression; please submit a bug report". I don't understand what I'm doing wrong.
To be clear, I would like to create a struct where I can just input an enum, a text string and an action for the button, and get back a Menu, to make the contentview more readable.
Hope you guys can help me. Cheers.
struct AddObjectMenuView<T: RawRepresentable, CaseIterable>: View {
let labelText: String
let someEnum: T
let function: () -> Void
var body: some View { // Getting error here
Menu {
ForEach(someEnum.allCases) { field in
Button(action: function) {
VStack {
Text(field.rawValue).padding()
}
}
}
} label: {
Text(labelText).padding().background(RoundedRectangle(cornerRadius: 10).foregroundColor(.clear))
}
.foregroundColor(Color("lightBlue"))
}
}
And then use it in my ContentView like this :
AddObjectMenuView(labelText: "Hello", someEnum: SomeEnum, function: {
// Do something
})
You need to work with type of generics type to use .allCases.
Here is fixed variant (modified parts are highlighted inline). Tested with Xcode 13.2 / iOS 15.2
struct AddObjectMenuView<T: CaseIterable & Hashable> : View where T.AllCases: RandomAccessCollection {
let labelText: String
let someEnum: T.Type // << here !!
let function: () -> Void
var body: some View {
Menu {
ForEach(someEnum.allCases, id: \.self) { field in // << here !!
Button(action: function) {
VStack {
Text(String(describing: field)).padding() // << here !!
}
}
}
} label: {
Text(labelText).padding().background(RoundedRectangle(cornerRadius: 10).foregroundColor(.clear))
}
.foregroundColor(Color("lightBlue"))
}
}
of course enums should be confirmed correspondingly.
And usage like
AddObjectMenuView(labelText: "Hello", someEnum: SomeEnum.self, function: {
// Do something
})

How can I infer generic parameter in extension?

I have a very simple CustomView which it takes a View and modify it! Like this code:
struct CustomView<ViewType: View>: View {
let content: () -> ViewType
var body: some View {
return content()
.foregroundColor(.red)
}
}
So far so good!
Now I want make a CustomModifier out of it, like this:
extension View {
func customModifier<ViewModifierType: View>(viewModifier: () -> ViewModifierType) -> some View {
// how can I import self from here?
return viewModifier()
}
}
use case:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.customModifier(viewModifier: { CustomView(content: { Text("Hello, World!") } ) }) // <<: Here is the issue!
// How can I satisfy CustomView with self in extension? that could help to cut Text("Hello, World!") to feeding as content for CustomView?
}
}
So if see the codes, all I am trying to do is cut off Text("Hello, World!") with using self from extension and trying to have this form of coding:
.customModifier(viewModifier: { CustomView() })
PS: I know that for having form of CustomView() I have to respect the generic parameter CustomView with word of where but I do not know how can I put these all puzzle together!
If I understand you correctly, then try this
extension View {
func customModifier<ViewModifierType: View>(viewModifier: (Self) -> ViewModifierType) -> some View {
return viewModifier(self)
}
}
Not sure what is your goal but if you just want to add a red foreground to a view you need to implement a view modifier:
struct RedForeground: ViewModifier {
func body(content: Content) -> some View {
content.foregroundColor(.red)
}
}
And then just call it using the modifier method:
Text("Text")
.modifier(RedForeground())
If you want to simplify it further you can extend View and add make it a computed property:
extension View {
var redForeground: some View { modifier(RedForeground()) }
}
usage:
Text("Text")
.redForeground
You can't just init CustomView without a view, but you can pass CustomView initializer as function to customModifier, like this:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.customModifier(viewModifier: CustomView.init)
}
}
struct CustomView<ViewType: View>: View {
let content: ViewType
var body: some View {
return content
.foregroundColor(.red)
}
}
extension View {
func customModifier<ViewModifierType: View>(viewModifier: (Self) -> ViewModifierType) -> some View {
return viewModifier(self)
}
}

How to add SwiftUI custom style as a static extension

In swiftui3 you can use buttonstyle shortcut like so
Button("0") {print("pressed 0")}
.buttonStyle(.bordered)
I would like to do that with my custom buttonstyle class
struct CrazyButtonStyle:ButtonStyle{
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.red)
}
}
calling it like this
Button("0") {print("pressed 0")}
.buttonStyle(.crazy)
I have tried
extension ButtonStyle{
static var crazy:CrazyButtonStyle {
get {
return CrazyButtonStyle()
}
}
}
but im getting this error
Contextual member reference to static property 'crazy' requires 'Self' constraint in the protocol extension
extension ButtonStyle where Self == CrazyButtonStyle{
static var crazy:CrazyButtonStyle {
get {
return CrazyButtonStyle()
}
}
}
adding where Self to the extension seems to work. But im not sure if this is the best way.
Apple's suggested way:
struct CrazyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(.red)
}
}
extension ButtonStyle where Self == CrazyButtonStyle {
static var crazy: Self { Self() }
}
Usage:
Button(action: {}) {
Image(systemName: "forward.fill")
}
.buttonStyle(.crazy)
Source: Develop Apps for iOS Tutorial
You can define something like the following:
struct CrazyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Spacer()
configuration.label.foregroundColor(.red)
Spacer()
}
.scaleEffect(configuration.isPressed ? 0.90 : 1)
}
}
Then, to apply it to a Button:
Button("Crazy Button") {}
.buttonStyle(CrazyButtonStyle())
See swift docs for reference.
Also here are some other examples, with animations as well!
/// define your style
public struct MainButtonStyle: ButtonStyle {
public init(){}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(height: 50, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color(.primary).clipShape(RoundedRectangle(cornerRadius: 4)))
.foregroundColor(Color.white)
.scaleEffect(configuration.isPressed ? 0.95: 1)
}
}
/// create an enum of all new types you've just created
public enum ButtonStyles {
case main
}
/// add a new functionality to the view
public extension View {
#ViewBuilder
func buttonStyle(_ style: ButtonStyles) -> some View {
switch style {
case .main:
self.buttonStyle(MainButtonStyle())
}
}
}
then you can use it like this
Button { someAction() } label: {Text("COPY")}
.buttonStyle(.main)