Function that returns View can't compile - swift

I want to return View depending on case.
#ViewBuilder
private func getView(case: Case) -> some View {
switch case {
case .case1:
View1()
case .case2:
View2()
case .case3:
View3()
}
}
I use it like this:
NavigationLink(destination: getView(case: case)) { ...
But this code can't compile, I get error
"The compiler is unable to type-check this expression in reasonable
time; try breaking up the expression into distinct sub-expressions"
I get this error even if I try to return just one View, like this:
#ViewBuilder
private func getView(case: Case) -> some View {
View1()
}
But if I use View directly in NavigationLink then everything works normally:
NavigationLink(destination: View1()) { ...
Why is this happening, and how to fix it?

If you really must call your variable case, or some other reserved word, enclose it in backticks, e.g.
#ViewBuilder
private func getView(case: Case) -> some View {
switch `case` {
case .case1:
View1()
case .case2:
View2()
case .case3:
View3()
}
}

Related

How to write a function that takes a closure of a view and returns a view?

I'm wanting to write utilities that work with views to do things like conditionally show views and do things like intersperse where a view is repeated and some kind of separator is inserted between each iteration. I can't figure out how to define the function type signature. This is what I got so far:
func ifNotLastCategory(_ cat: String, content: () -> AnyView) -> AnyView {
if (cat != movie.categories.last) { return content() }
}
...
ifNotLastCategory(category) { Text("Hello World") }
When I try to do something like that I get a compiler error about Cannot convert value of type 'some View' to closure result type 'AnyView'. However it won't let me define content as returning some View.
How can I make this function work?
Try not to use AnyView unless you really have a good reason for it. In most cases you can use other solutions which are more efficient and cleaner.
In your case I suggest using a #ViewBuilder and returning some View:
#ViewBuilder
func ifNotLastCategory<V: View>(_ cat: String, content: () -> V) -> some View {
if cat != movie.categories.last {
content()
}
}
You can use a generic function and cast the result to AnyView:
func ifNotLastCategory<V: View>(_ cat: String, content: () -> V) -> AnyView {
if cat != movie.categories.last { return AnyView(content()) }
return AnyView(EmptyView())
}

SwiftUI withAnimation causes unexpected behaviour

I noticed a strange behaviour of XCode using swiftUI's withAnimation{}.
I created the following working example:
import SwiftUI
class Block: Hashable, Identifiable {
// Note: this needs to be a class rather than a struct
var id = UUID()
static func == (lhs: Block, rhs: Block) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
}
struct ContentView: View {
#State private var blocks: [Block]
init() {
var blocks = [Block]()
/// generate some blocks here
blocks.append(Block())
blocks.append(Block())
blocks.append(Block())
self._blocks = State(initialValue: blocks)
}
var body: some View {
VStack {
ForEach(blocks, id: \.self) { block in
BlockRow(block: block) { b in
print("COMMENT THIS LINE OUT") // try commenting out this line -> XCODE won't build anymore
withAnimation {
self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
}
}
}
}
}
struct BlockRow: View {
#State var block: Block
var onDelete: (Block) -> Void = {_ in}
var body: some View {
Text("<Block>")
.onTapGesture {
print("Tap block \(block.id)")
self.onDelete(block)
}
}
}
The above example works as expected, if you click on one of the blocks it get's deleted and the following blocks will nicly slide in the free position.
But XCode produces a warning here, I do not understand:
Result of call to 'withAnimation' is unused
Things get even more confusing: by just commenting out the print stamenet beforehand the withAnimation block /* print("COMMENT THIS LINE OUT") */ XCode will no longer build the project:
Failed to produce diagnostic for expression; please file a bug report
Is this a bug or am I completely off-road with my aproach?
The origin of the problem is the fact that self.blocks.remove(at:) returns a value that you are not handling. If you explicitly ignore that value, then your code works as expected whether or not the print statement is there.
withAnimation {
_ = self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
Swift has a feature that a closure with a single line of code returns the value of that statement as the return of the closure. So in this case, if you don't ignore the return value of remove(at:) it returns a Block and Block then becomes the returned type of the withAnimation closure.
withAnimation is defined like this:
func withAnimation<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result
It is a generic function that takes a closure. The return type of that closure determines the type of the generic placeholder which in turn determines the return type of withAnimation itself.
So, if you don't ignore the return type of remove(at:), the withAnimation<Block> function will return a Block.
If you ignore the return value of remove(at:), the statement becomes one that has no return, (that is, it returns () or Void). Thus, the withAnimation function become withAnimation<Void> and it returns Void.
Now, because the closure to BlockRow has only a single line of code when you delete the print statement, its return value is the return value of the single statement, which is now Void:
BlockRow(block: block) { b in
withAnimation {
_ = self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
}
and this matches the type that the closure to BlockRow is expecting for its onDelete closure.
In your original code, the print statement caused the closure to BlockRow to have 2 lines of code, thus avoiding Swift's feature of using the single line of code to determine the return type of the closure. You did get a warning that you weren't using the Block that was being returned from the withAnimation<Block> function. #Asperi's answer fixed that warning by assigning the Block returned by withAnimation<Block> to _. This has the same effect as my suggested solution, but it is handling the problem one level higher instead of at the source of the problem.
Why doesn't the compiler complain that you are ignoring the return value of remove(at:)?
remove(at:) is explicitly designed to allow you to discard the returned result which is why you don't get a warning that it returns a value that you aren't handling.
#discardableResult mutating func remove(at i: Self.Index) -> Self.Element
But as you see, this lead to the confusing result you encountered. You were using remove(at:) as if it returned Void, but it in fact was returning the Block that was removed from your array. This then lead to the whole chain of events that lead to your issue.
Use the following
var body: some View {
VStack {
ForEach(blocks, id: \.self) { block in
BlockRow(block: block) { b in
_ = withAnimation {
self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
}
}
}
}
Tested with Xcode 12.0 / iOS 14

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