Generic enum Menu SwiftUI - swift

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

Related

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

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.")
}
}
}

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

macOS - Error while passing information through UI View

I am trying to pass information from one SwiftUI View (let's say View1) to another one (View2) using NavigationLink. I have found this article, but when I try to run it I get the error "Cannot find type 'Event' in scope":
Here is my code for View1:
#available(OSX 11.0, *)
struct Mods_UI: View {
let PassMe = "Hello!"
var body: some View {
NavigationView {
NavigationLink(destination: MarkdownView(item: String(PassMe))) {
Label("Hello World!")
}
}
}
}
Here is my code for View2:
struct MarkdownView: View {
let item: Event
var body: some View {
Text(String(item))
}
}
I have no idea what is going wrong. If more information is needed, please tell me.
Swift Version: Latest (5.3.3)
Xcode Version: Latest (12.4)
In the page you linked to, they have a custom type called Event, probably defined as a struct somewhere in their code that was not included in the post.
You, however are just trying to pass a String around. You can do this:
#available(OSX 11.0, *)
struct Mods_UI: View {
let passMe = "Hello!"
var body: some View {
NavigationView {
NavigationLink(destination: MarkdownView(item: passMe)) {
Label("Hello World!")
}
}
}
}
struct MarkdownView: View {
var item: String
var body: some View {
Text(item)
}
}

Deselecting item from a picker SwiftUI

I use a form with a picker, and everything works fine (I am able to select an element from the picker), but I cannot deselect it. Does there exist a way to deselect an item from the picker?
Thank you!
Picker(selection: $model.countries, label: Text("country")) {
ForEach(model.countries, id: \.self) { country in
Text(country!.name)
.tag(country)
}
}
To deselect we need optional storage for picker value, so here is a demo of possible approach.
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State private var value: Int?
var body: some View {
NavigationView {
Form {
let selected = Binding(
get: { self.value },
set: { self.value = $0 == self.value ? nil : $0 }
)
Picker("Select", selection: selected) {
ForEach(0...9, id: \.self) {
Text("\($0)").tag(Optional($0))
}
}
}
}
}
}
I learned almost all I know about SwiftUI Bindings (with Core Data) by reading this blog by Jim Dovey. The remainder is a combination of some research and many hours of making mistakes.
So when I combine Jim's technique to create Extensions on SwiftUI Binding with Asperi's answer, then we end up with something like this...
public extension Binding where Value: Equatable {
init(_ source: Binding<Value>, deselectTo value: Value) {
self.init(get: { source.wrappedValue },
set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
)
}
}
Which can then be used throughout your code like this...
Picker("country", selection: Binding($selection, deselectTo: nil)) { ... }
OR
Picker("country", selection: Binding($selection, deselectTo: someOtherValue)) { ... }
First of, we can fix the selection. It should match the type of the tag. The tag is given Country, so to have a selection where nothing might be selected, we should use Country? as the selection type.
It should looks like this:
struct ContentView: View {
#ObservedObject private var model = Model()
#State private var selection: Country?
var body: some View {
NavigationView {
Form {
Picker(selection: $selection, label: Text("country")) {
ForEach(model.countries, id: \.self) { country in
Text(country!.name)
.tag(country)
}
}
Button("Clear") {
selection = nil
}
}
}
}
}
You then just need to set the selection to nil, which is done in the button. You could set selection to nil by any action you want.
If your deployment target is set to iOS 14 or higher -- Apple has provided a built-in onChange extension to View where you can deselect your row using tag, which can be used like this instead (Thanks)
Picker(selection: $favoriteColor, label: Text("Color")) {
// ..
}
.onChange(of: favoriteColor) { print("Color tag: \($0)") }

SwiftUI NavigationLink - "lazy" destination? Or how to not code duplicate `label`?

(on macOS Big Sur with Xcode 12 beta)
One thing I've been struggling with in SwiftUI is navigating from e.g. a SetUpGameView which needs to create a Game struct depending on e.g. player name inputted by the user in this view and then proceed with navigation to GameView via a NavigationLink, being button-like, which should be disabled when var game: Game? is nil. The game cannot be initialized until all necessary data has been inputted by the user, but in my example below I just required playerName: String to be non empty.
I have a decent solution, but it seems too complicated.
Below I will present multiple solutions, all of which seems too complicated, I was hoping you could help me out coming up with an even simpler solution.
Game struct
struct Game {
let playerName: String
init?(playerName: String) {
guard !playerName.isEmpty else {
return nil
}
self.playerName = playerName
}
}
SetUpGameView
The naive (non-working) implementation is this:
struct SetUpGameView: View {
// ....
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
// ...
NavigationLink(
destination: GameView(game: game!),
label: { Label("Start Game", systemImage: "play") }
)
.disabled(game == nil)
// ...
}
}
// ...
}
However, this does not work, because it crashes the app. It crashes the app because the expression: GameView(game: game!) as destionation in the NavigationLink initializer does not evaluate lazily, the game! evaluates early, and will be nil at first thus force unwrapping it causes a crash. This is really confusing for me... It feels just... wrong! Because it will not be used until said navigation is used, and using this particular initializer will not result in the destination being used until the NavigationLink is clicked. So we have to handle this with an if let, but now it gets a bit more complicated. I want the NavigationLink label to look the same, except for disabled/enabled rendering, for both states game nil/non nil. This causes code duplication. Or at least I have not come up with any better solution than the ones I present below. Below is two different solutions and a third improved (refactored into custom View ConditionalNavigationLink View) version of the second one...
struct SetUpGameView: View {
#State private var playerName = ""
init() {
UITableView.appearance().backgroundColor = .clear
}
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player name", text: $playerName)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
// All these three solution work
// navToGameSolution1
// navToGameSolution2
navToGameSolution2Refactored
}
}
}
// MARK: Solutions
// N.B. this helper view is only needed by solution1 to avoid duplication in if else, but also used in solution2 for convenience. If solution2 is used this code could be extracted to only occur inline with solution2.
var startGameLabel: some View {
// Bug with new View type `Label` see: https://stackoverflow.com/questions/62556361/swiftui-label-text-and-image-vertically-misaligned
HStack {
Image(systemName: "play")
Text("Start Game")
}
}
var navToGameSolution1: some View {
Group { // N.B. this wrapping `Group` is only here, if if let was inline in the `body` it could have been removed...
if let game = game {
NavigationLink(
destination: GameView(game: game),
label: { startGameLabel }
)
} else {
startGameLabel
}
}
}
var navToGameSolution2: some View {
NavigationLink(
destination: game.map { AnyView(GameView(game: $0)) } ?? AnyView(EmptyView()),
label: { startGameLabel }
).disabled(game == nil)
}
var navToGameSolution2Refactored: some View {
NavigatableIfModelPresent(model: game) {
startGameLabel
} destination: {
GameView(game: $0)
}
}
}
NavigatableIfModelPresent
Same solution as navToGameSolution2 but refactored, so that we do not need to repeat the label, or construct multiple AnyView...
struct NavigatableIfModelPresent<Model, Label, Destination>: View where Label: View, Destination: View {
let model: Model?
let label: () -> Label
let destination: (Model) -> Destination
var body: some View {
NavigationLink(
destination: model.map { AnyView(destination($0)) } ?? AnyView(EmptyView()),
label: label
).disabled(model == nil)
}
}
Feels like I'm missing something here... I don't want to automatically navigate when game becomes non-nil and I don't want the NavigationLink to be enabled until game is non nil.
You could try use #Binding instead to keep track of your Game. This will mean that whenever you navigate to GameView, you have the latest Game as it is set through the #Binding and not through the initializer.
I have made a simplified version to show the concept:
struct ContentView: View {
#State private var playerName = ""
#State private var game: Game?
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player Name", text: $playerName) {
setGame()
}
}
NavigationLink("Go to Game", destination: GameView(game: $game))
Spacer()
}
}
}
private func setGame() {
game = Game(title: "Some title", playerName: playerName)
}
}
struct Game {
let title: String
let playerName: String
}
struct GameView: View {
#Binding var game: Game!
var body: some View {
VStack {
Text(game.title)
Text(game.playerName)
}
}
}