Swift switch case simplification - swift

I am currently creating an iOS app based on the math card game "Set", and I am running into a problem simplifying the switch case statement. My goal is to avoid an ugly series of nested switches in my view. I come from a background of OOP and normally I would store references to an object and call them later where I want; however, I am trying to be functional. Below is a some code describing roughly what I would like to do.
struct CardView: View {
var card: SetGame<CardShape, CardShading, CardColor>.Card
var cardShape: some Shape {
switch card.shape {
case .diamond:
return Diamond()
case .squiggle:
return Squiggle()
case .oval:
return Oval()
}
}
var cardShading: some ViewModifier {
switch card.shading {
case .open:
return .stroke()
case .striped:
return .stripe()
case .solid:
return .fill()
}
}
var cardColor: Color
{
switch card.color {
case .red:
return Color.red
case .green:
return Color.green
case .purple:
return Color.purple
}
}
var body: some View {
VStack {
ForEach(0..<card.count) { _ in
cardShape.cardShading
}
.aspectRatio(contentAspectRatio, contentMode: .fit)
}
.foregroundColor(cardColor)
.padding()
.cardify(selected: card.selected)
}
// MARK: Drawing Constants
let contentAspectRatio: CGSize = CGSize(width: 2, height: 1)
}
Now the above code does not work for obvious reasons, it is more about conveying what I am trying to accomplish. The easy brute force solution I see would be to have three nested switch/case statements, but I have to believe there is another way. I tried using type erasure in card shape doing something like this...
var cardShape: some View {
switch card.shape {
case .diamond:
return AnyView(Diamond())
case .squiggle:
return AnyView(Squiggle())
case .oval:
return AnyView(Oval())
}
}
and while this does "work", it is no longer a shape and I lose access to my shape modifiers (ie. fill, stripe, stroke). I am pretty sure I won't be able to store my ViewModifiers and call them at a different place, so I am sort of expecting there to be one switch/case inside of my var body. If someone has a completely different approach or if I am just doing something stupid, feel free to let me know! I am trying to learn the most "Swiftish" way of doing things. Thanks in advance!

Related

How can I replicate WindowButton actions in SwiftUI for macOS?

I want replicate WindowButton actions like close, minimize and maximize with this code, but I have lots of issue and errors, like:
struct ContentView: View {
var body: some View {
Button("close") {
WindowButtonEnum.close.actionFunction(value: ContentView.self)
}
Button("miniaturize") {
WindowButtonEnum.miniaturize.actionFunction(value: ContentView.self)
}
Button("zoom") {
WindowButtonEnum.zoom.actionFunction(value: ContentView.self)
}
}
}
enum WindowButtonEnum {
case close, miniaturize, zoom
func actionFunction(value: Any?) {
switch self {
case .close: NSApplication.shared.keyWindow?.close
case .miniaturize: NSWindow.miniaturize(value)
case .zoom: NSWindow.performZoom(value)
}
}
}
Function is unused
Instance member 'miniaturize' cannot be used on type 'NSWindow'; did you mean to use a value of this type instead?
Instance member 'performZoom' cannot be used on type 'NSWindow'; did you mean to use a value of this type instead?
I want find the answer with using enum and more SwiftUI-isch coding style.

Convert from switch case statement to a value in Swift

Maybe this is a stupid question
I have a switch case statement like this:
self.text = type.rawValue
switch type {
case .teuro:
self.backgroundColor = UIColor.sapphireColor()
case .lesson:
self.backgroundColor = UIColor.orangeColor()
case .profession:
self.backgroundColor = UIColor.pinkyPurpleColor()
}
Is there any way to write it in something like this example:
self.backgroundColor = {
switch type {
case .teuro:
return UIColor.sapphireColor()
case .lesson:
return UIColor.orangeColor()
case .profession:
return UIColor.pinkyPurpleColor()
}
}
Any comment or answer is appreciated. Thanks.
You are nearly there!
You've created a closure that returns a color, and are assigning it to a UIColor property, but a closure is not a color!
You just need to call (invoke) the closure to run it, so that it returns the color you want:
self.backgroundColor = {
switch type {
case .teuro:
return UIColor.sapphireColor()
case .lesson:
return UIColor.orangeColor()
case .profession:
return UIColor.pinkyPurpleColor()
}
}() // <---- notice the brackets!
Usually you don't wanna mix your models and your UI, and only need to use this colors inside one view
That's why I ended up creating such extensions, which are only accessible inside current file of the view:
private extension Type {
var backgroundColor: UIColor {
switch self {
case .teuro:
return .sapphireColor()
case .lesson:
return .orangeColor()
case .profession:
return .pinkyPurpleColor()
}
}
}
Sweeper is absolutely correct, that you can initialize this with a closure by adding the missing () at the end. So that is the literal answer to your question.
But Philip also is correct, that it would be best to add an extension to your enumeration type to define a mapping between colors and your cases. It abstracts the color scheme from both the calling point (e.g. ensuring that you have a consistent application of colors throughout the app, while never repeating yourself), but at the same time, avoids entangling the UI color scheme with some basic enumeration type.
But I would like to take it a step further, namely to extend this observation to the text property, too. You should not use the rawValue strings in your UI. The raw codes (if you have them at all) should not be conflated with the strings you want to display in the UI. One is a coding question, while the other is a UI question.
So, I would not only move color into an extension, but also the display text, e.g. I would define a text property:
extension MyEnumerationType {
var text: String { rawValue }
}
Then you could do:
self.text = myEnumerationInstance.text
Now here I am still using rawValue, but I am abstracting it away from the UI. The reason is that you might want to eventually support different strings, but not change your rawValue codes. E.g., you might want to support localization at some point:
extension MyEnumerationType {
var text: String { NSLocalizedString(rawValue, comment: "MyEnumerationType") }
}
Or you might have a switch statement inside this text computed property. But it avoids the tight coupling of your enumeration’s internal representation (the rawValue) from the UI.
So, bottom line, not only should you abstract the color scheme out of the type itself, but you should abstract display text, too. That way, you can change your display text at some future date, but not break code that relied on the old rawValue values.
It's better to create one color var inside the enum.
Here is the example
enum Type {
case type1, type2
var color: UIColor {
switch self {
case .type1:
return .black
case .type2:
return .yellow
}
}
}
Use
class ViewController: UIViewController {
let type: Type = .type1
override func viewDidLoad() {
self.view.backgroundColor = type.color
}
}

Modifying SwiftUI Views based on non-binary data / conditions?

Question: I have a fairly large SwiftUI View that adds a country code based on my model data. My feeling is that I should extract a subview and then pass the country code in, then use a switch, but I just wanted to check I was not missing something and making this too complicated.
SwiftUI has a very nice method of dealing with two possible options based on a Bool, this is nice as it modifies a single View.
struct TestbedView_2: View {
var isRed: Bool
var body: some View {
Text("Bilbo").foregroundColor(isRed ? Color.red : Color.blue)
}
}
If on the other hand your model presents a none binary choice you can use a switch statement. This however returns an Independent View based on the case selected resulting in duplicate code.
struct TestbedView_3: View {
var index: Int
var body: some View {
switch(index) {
case 1:
Text("Bilbo").foregroundColor(Color.red)
case 2:
Text("Bilbo").foregroundColor(Color.green)
default:
Text("Bilbo").foregroundColor(Color.blue)
}
}
}
Here is more clean approach:
Only display text once - use function to get the color
Text("Bilbo").foregroundColor(self.getColor(index))
Create the getColor function
private func getColor(_ index : Int) -> Color {
switch index {
case 1: return Color.red
case 2: return Color.green
case 3: return Color.blue
default: return Color.clear
}
}
NOTE: I am using Color.clear as default case in the switch statement since it must be present

Function declares an opaque return type, but the return statements in its body do not have matching underlying types [duplicate]

Ok, SwiftUI was released this week so we're all n00bs but... I have the following test code:
var body: some View {
switch shape {
case .oneCircle:
return ZStack {
Circle().fill(Color.red)
}
case .twoCircles:
return ZStack {
Circle().fill(Color.green)
Circle().fill(Color.blue)
}
}
}
which produces the following error:
Function declares an opaque return type, but the return statements in its body do not have matching underlying types
This happens because the first ZStack is this type:
ZStack<ShapeView<Circle, Color>>
and the second is this type:
ZStack<TupleView<(ShapeView<Circle, Color>, ShapeView<Circle, Color>)>>
How do I deal with this in SwiftUI? Can they be flattened somehow or be made to conform to the same type.
One way to fix this is to use the type eraser AnyView:
var body: some View {
switch shape {
case .oneCircle:
return AnyView(ZStack {
Circle().fill(Color.red)
})
case .twoCircles:
return AnyView(ZStack {
Circle().fill(Color.green)
Circle().fill(Color.blue)
})
}
}
UPDATE
I add the following to answer the commenters who are asking why this is needed.
One commenter says
ZStack is still a View, right?
Actually, no. ZStack by itself is not a View. ZStack<SomeConcreteView> is a View.
The declaration of ZStack looks like this:
public struct ZStack<Content> : View where Content : View
ZStack is generic. That means that ZStack by itself is not a type. It is a “type constructor”.
The idea of a type constructor is not usually discussed in the Swift community. A type constructor is, essentially, a function that runs at compile time. The function takes one or more types as arguments and returns a type.
ZStack is a type constructor that takes one argument. If you ‘call’ ZStack repeatedly with different arguments, it returns different answers. This is what Robert Gummesson shows in his question:
This happens because the first ZStack is this type:
ZStack<ShapeView<Circle, Color>>
and the second is this type:
ZStack<TupleView<(ShapeView<Circle, Color>, ShapeView<Circle, Color>)>>
In the first case, the program ‘calls’ ZStack with the argument ShapeView<Circle, Color> and gets back a type as the answer. In the second case, the program ‘calls’ ZStack with a different argument, TupleView<(ShapeView<Circle, Color>, ShapeView<Circle, Color>)>, and so it gets back a different type as the answer.
The declaration var body: some View says that the body method returns a specific, concrete type (to be deduced by the compiler) that conforms to the View protocol. Since the two ‘calls’ to ZStack return different concrete types, we must find a way to convert them both to a single common type. That is the purpose of AnyView. Note that AnyView is not generic, which is to say, it is not a type constructor. It is just a plain type.
You can also use Group which is logical container so won't change anything visual.
var body: some View {
Group {
switch shape {
case .oneCircle:
return ZStack {
Circle().fill(Color.red)
}
case .twoCircles:
return ZStack {
Circle().fill(Color.green)
Circle().fill(Color.blue)
}
}
}
}
I know this is an old question. But i stumbled upon it, and the now the correct way to do it, would be with a #ViewBuilder annotation (notice the missing return):
#ViewBuilder var body: some View {
switch shape {
case .oneCircle:
ZStack {
Circle().fill(Color.red)
}
case .twoCircles:
ZStack {
Circle().fill(Color.green)
Circle().fill(Color.blue)
}
}
}
Or in this specific case, factor out the ZStack:
var body: some View {
ZStack {
switch shape {
case .oneCircle:
Circle().fill(Color.red)
case .twoCircles:
Circle().fill(Color.green)
Circle().fill(Color.blue)
}
}
}

SwiftUI strange behavior when moving items between sections in a List

so I've been trying to make a component using swiftUI that allows you to move items in a List between sections.
I prepared an example with two sections: "First List" and "Second List". Whenever you tap on an item it swaps sections. Here's a screenshot:
When I tap on "First List: 1", it correctly moves to the second section:
However, its name should now be changed to "Second List: 1" because of the way I named the elements in the sections (see code below). So that's strange. But it gets stranger:
When I now tap on "First List: 1" in the second section this happens:
It doesn't properly swap back. It just gets duplicated, but this time the name of the duplicate is actually correct.
Considering the code below I don't understand how this is possible. It seems that swiftUI somehow reuses the item, even though it re-renders the view? It also seems to reuse the .onTapGesture closure, because the method that's supposed to put the item back into the first section is never actually called.
Any idea what's going on here? Below is a fully working example of the problem:
import SwiftUI
import Combine
struct TestView: View {
#ObservedObject var viewModel: ViewModel
class ViewModel: ObservableObject {
let objectWillChange = PassthroughSubject<ViewModel,Never>()
public enum List {
case first
case second
}
public var first: [Int] = []
public var second: [Int] = []
public func swap(elementWithIdentifier identifier: Int, from list: List) {
switch list {
case .first:
self.first.removeAll(where: {$0 == identifier})
self.second.append(identifier)
case .second:
print("Called")
self.second.removeAll(where: {$0 == identifier})
self.first.append(identifier)
}
self.objectWillChange.send(self)
}
init(first: [Int]) {
self.first = first
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("First List")) {
ForEach(self.viewModel.first, id: \.self) { id in
Text("First List: \(id)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
self.viewModel.swap(elementWithIdentifier: id, from: .first)
}
}
}
Section(header: Text("First List")) {
ForEach(self.viewModel.second, id: \.self) { id in
Text("Second List: \(id)")
.onTapGesture {
self.viewModel.swap(elementWithIdentifier: id, from: .second)
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Testing"))
}.environment(\.editMode, .constant(EditMode.active))
}
}
struct TestView_Preview: PreviewProvider {
static var previews: some View {
TestView(viewModel: TestView.ViewModel(first: [1, 2, 3, 4, 5]))
}
}
The only way I've solved this is to prevent diffing of the list by adding a random id to the list. This removes animations though, so looking for a better solution
List {
...
}
.id(UUID())
Removing the sections also fixes this, but isn't a valid solution either
I've found myself in a similar situation and have a found a more elegant workaround to this problem. I believe the issue lies with iOS13. In iOS14 the problem no longer exists. Below details a simple solution that works on both iOS13 and iOS14.
Try this:
extension Int {
var id:UUID {
return UUID()
}
}
and then in your ForEach reference \.id or \.self.id and not \.self i.e like so in both your Sections:
ForEach(self.viewModel.first, id: \.id) { id in
Text("First List: \(id)")
.onTapGesture {
self.viewModel.swap(elementWithIdentifier: id, from: .first)
}
}
This will make things work. However, when fiddling around I did find these issues:
Animations were almost none existent in iOS14. This can be fixed though.
In iOS13 the .listStyle(GroupedListStyle()) animation looks odd. Remove this and animations look a lot better.
I haven't tested this solution on large lists. So be warned around possible performance issues. For smallish lists it works.
Once again, this is a workaround but I think Apple is still working out the kinks in SwiftUI.
Update
PS if you use any onDelete or onMove modifiers in iOS14 this adds animations to the list which causes odd behaviour. I've found that using \.self works for iOS14 and \.self.id for iOS13. The code isn't pretty because you'll most likely have #available(iOS 14.0, *) checks in your code. But it works.
I don't know why, but it seems like your swap method does something weird on the first object you add, because if the second one works, maybe you've lost some instance.
By the way, do you need to removeAll every time you add a new object in each list?
public function interchange (identifier elementWithIdentifier: Int, from list: List) {
switch list {
case .first:
self.first.removeAll (where: {$ 0 == identifier})
self.second.append (identifier)
case .second:
print ("Called")
self.second.removeAll (where: {$ 0 == identifier})
self.first.append (identifier)
}
self.objectWillChange.send (self)
}
maybe your problem is in this function, everything looks great.
The fix is simple - use default ObservableObject publishers (which are correctly observed by ObservedObject wrapper) instead of Combine here, which is not valid for this case.
class ViewModel: ObservableObject {
public enum List {
case first
case second
}
#Published public var first: [Int] = [] // << here !!
#Published public var second: [Int] = [] // << here !!
public func swap(elementWithIdentifier identifier: Int, from list: List) {
switch list {
case .first:
self.first.removeAll(where: {$0 == identifier})
self.second.append(identifier)
case .second:
print("Called")
self.second.removeAll(where: {$0 == identifier})
self.first.append(identifier)
}
}
init(first: [Int]) {
self.first = first
}
}
Tested with Xcode 13.3 / iOS 15.4
*and even with animation wrapping swap into withAnimation {}