SwiftUI doesn't like switch? - swift

Trying to following this discussion, I implemented the suggestion of Yurii Kotov:
struct ContentView: View {
#State private var index = 0
#ViewBuilder
var body: some View {
if index == 0 {
MainView()
} else {
LoginView()
}
}
It works fine. But if I try to use a switch statement instead:
switch index {
case 0: MainView()
case 1: LoginView()
default:
print("# error in switch")
}
nothing happens. There is no mistake alert, but also no result at all. Could somebody help?

I had an issue with the default case where I wanted a "break" type situation for the switch to be exhaustive. SwiftUI requires some type of view, I found that
EmptyView() solved the issue.
also noted here EmptyView Discussion that you "have to return something"
struct FigureListMenuItems: View {
var showContextType: ShowContextEnum
#Binding var isFavoriteSeries: Bool?
var body: some View {
Menu {
switch showContextType {
case .series:
Button(action: { toggleFavoriteSeries() }) {
isFavoriteSeries! ?
Label("Favorite Series?", systemImage: "star.fill")
.foregroundColor(.yellow)
:
Label("Favorite Series?", systemImage: "star")
.foregroundColor(.yellow)
}
default: // <-- can use EmptyView() in this default too if you want
Button(action: {}) {
Text("No menu items")
}
}
} label: {
switch showContextType {
case .series:
isFavoriteSeries! ?
Image(systemName: "star.fill")
.foregroundColor(.yellow)
:
Image(systemName: "star")
.foregroundColor(.yellow)
default:
EmptyView()
}
Label("Menu", systemImage: "ellipsis.circle")
}
}
private func toggleFavoriteSeries() {
isFavoriteSeries?.toggle()
}
}
Enum for switch
enum ShowContextEnum: Int {
case series = 1
case whatIsNew = 2
case allFigures = 3
}

As #Sweeper said: your if...else and switch...case statements are not equal. The main idea is: body is just a computed variable of a View protocol and it should return something of its' type. Sure, you can do it with switch...case statements. In your code snippet mistake is in default statement: body cannot return() -> Void, only some View. So your code should looks like this:
struct ViewWithSwitchStatements: View {
#State var option = 0
var body: some View {
switch option {
case 1:
return AnyView(Text("Option 1"))
case 2:
return AnyView(Text("Option 2"))
default:
return AnyView(Text("Wrong option!"))
}
}
}
However you can't put it into VStack or something else, like if...else statements.

Related

Open sheet and overwrite current sheet or provide internal navigationstack inside sheet

So I have a button as shown below:
private var getStartedButton: some View {
Button {
showGetStartedSheet.toggle()
} label: {
Text("Get Started")
}
.sheet(isPresented: $showGetStartedSheet) {
LoginUserSheetView()
.presentationDetents([.fraction(0.70), .large])
}
}
Which opens the LoginUserSheetView() view and has the following function inside:
private var userCreateAccount: some View {
VStack(alignment: .center) {
Text("New MapGliders? User register")
.sheet(isPresented: $showUserRegisterSheet) {
RegisterUserSheetView()
.presentationDetents([.fraction(0.70), .large])
}
}
}
The above code, then opens another sheet which presents the following code:
private var appleButton: some View {
Button {
// Hello
} label: {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "applelogo")
Text("Hello")
}
}
}
The above code (lots has been removed) produces the following output:
https://im4.ezgif.com/tmp/ezgif-4-4ecfdb6d55.gif
As you can see the video above, the second sheet opens on top of the old sheet, I would like the sheet to be overwritten or create a navigation on a single sheet.
Does anyone know how I can close LoginUserSheetView() when RegisterUserSheetView() is opened? or how could I make the sheet be overwritten or even use a navigation to navigate to RegisterUserSheetView() when on the LoginUserSheetView() is opened.
The first option is to use sheet(item:). But this does dismiss the sheet and they makes it reappear with the new value
struct DynamicOverlay: View {
#State var selectedOverlay: OverlayViews? = nil
var body: some View {
VStack{
Text("hello there")
Button("first", action: {
selectedOverlay = .first
})
}
.sheet(item: $selectedOverlay){ passed in
passed.view($selectedOverlay)
}
}
enum OverlayViews: String, Identifiable{
var id: String{
rawValue
}
case first
case second
#ViewBuilder func view(_ selectedView: Binding<OverlayViews?>) -> some View{
switch self{
case .first:
ZStack {
Color.blue
.opacity(0.5)
Button {
selectedView.wrappedValue = .second
} label: {
Text("Next")
}
}
case .second:
ZStack {
Color.red
.opacity(0.5)
Button("home") {
selectedView.wrappedValue = nil
}
}
}
}
}
}
The second option doesn't have the animation behavior but required a small "middle-man" View
import SwiftUI
struct DynamicOverlay: View {
#State var selectedOverlay: OverlayViews = .none
#State var presentSheet: Bool = false
var body: some View {
VStack{
Text("hello there")
Button("first", action: {
selectedOverlay = .first
presentSheet = true
})
}
.sheet(isPresented: $presentSheet){
//Middle-main
UpdatePlaceHolder(selectedOverlay: $selectedOverlay)
}
}
enum OverlayViews{
case first
case second
case none
#ViewBuilder func view(_ selectedView: Binding<OverlayViews>) -> some View{
switch self{
case .first:
ZStack {
Color.blue
.opacity(0.5)
Button {
selectedView.wrappedValue = .second
} label: {
Text("Next")
}
}
case .second:
ZStack {
Color.red
.opacity(0.5)
Button("home") {
selectedView.wrappedValue = .none
}
}
case .none:
EmptyView()
}
}
}
//Needed to reload/update the selected item on initial presentation
struct UpdatePlaceHolder: View {
#Binding var selectedOverlay: OverlayViews
var body: some View{
selectedOverlay.view($selectedOverlay)
}
}
}
You can read a little more on why you need that intermediate view here SwiftUI: Understanding .sheet / .fullScreenCover lifecycle when using constant vs #Binding initializers

How to assign value to #State in View from ViewModel?

I have a movie listing view with basic listing functionality, Once pagination reaches to the last page I want to show an alert for that I am using reachedLastPage property.
The viewModel.state is an enum, the case movies has associated value in which there is moreRemaining property which tells if there are more pages or not.
Once the moreRemaining property becomes false I want to make reachedLastPage to true so that I can show an alert.
How can I achieve this in best way?
import SwiftUI
import SwiftUIRefresh
struct MovieListingView<T>: View where T: BaseMoviesListViewModel {
#ObservedObject var viewModel: T
#State var title: String
#State var reachedLastPage: Bool = false
var body: some View {
NavigationView {
ZStack {
switch viewModel.state {
case .loading:
LoadingView(title: "Loading Movies...")
.onAppear {
fetchMovies()
}
case .error(let error):
ErrorView(message: error.localizedDescription, buttonTitle: "Retry") {
fetchMovies()
}
case .noData:
Text("No data")
.multilineTextAlignment(.center)
.font(.system(size: 20))
case .movies(let data):
List {
ForEach(data.movies) { movie in
NavigationLink(destination: LazyView(MovieDetailView(viewModel: MovieDetailViewModel(id: movie.id)))) {
MovieViewRow(movie: movie)
.onAppear {
if movie == data.movies.last && data.moreRemaining {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
fetchMovies()
}
}
}
}
if movie == data.movies.last && data.moreRemaining {
HStack {
Spacer()
ActivityIndicator(isAnimating: .constant(data.moreRemaining))
Spacer()
}
}
}
}.pullToRefresh(isShowing: .constant(data.isRefreshing)) {
print("Refresheeeee")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
refreshMovies()
}
}
}
}
.navigationViewStyle(.stack)
.navigationBarTitle("\(title)", displayMode: .inline)
.alert(isPresented: $reachedLastPage) {
Alert(title: Text("You have reached to the end of the list."))
}
}
}
private func fetchMovies() {
viewModel.trigger(.fetchMovies(false))
}
private func refreshMovies() {
viewModel.trigger(.fetchMovies(true))
}
}
you could try this approach, using .onReceive(...). Add this to your
ZStack or NavigationView:
.onReceive(Just(viewModel.moreRemaining)) { val in
reachedLastPage = !val
}
Also add: import Combine
(Ignoring "the best way" part, 'cause it's opinion-based,) one way to achieve that is to make your view model an observable object (which likely already is), adding the publisher of reachedLastPage there, and observe it directly from the view. Something like this:
final class ContentViewModel: ObservableObject {
#Published var reachedLastPage = false
init() {
// Just an example of changing the value.
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { self.reachedLastPage = true }
}
}
struct ContentView: View {
var body: some View {
Text("Hello World")
.alert(isPresented: $viewModel.reachedLastPage) {
Alert(title: Text("Alert is triggered"))
}
}
#ObservedObject private var viewModel: ContentViewModel
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
}
Once reachedLastPage takes the true value, the alert will be presented.

Extracting Observed Object into subview behaves differently than just one view struct. Why?

This doesn't work the way I intended it…
When an error occurs in MyClass instance the user is advised to check the settings app and then come back to my app. The next time my app is opened it should just retry by initializing MyClass all over again. If the error persists it will just again display above everything else. (Actually I would like to just fatalError() my app but that isn't best practice, is it?) So I thought I just initialize a new instance of MyClass…
class MyClass: ObservableObject {
static var shared = MyClass()
#Published var errorMsg: String? = nil
func handleError() -> Void {
DispatchQueue.main.async {
self.errorMsg = "Sample Error Message"
}
}
init() {
self.errorMsg = nil
}
}
struct ContentView: View {
#ObservedObject var theObj = MyClass.shared
#Environment(\.scenePhase) private var scenePhase
var body: some View {
ZStack {
VStack {
Text("App when everything is fine")
.onTapGesture {
MyClass.shared.handleError()
}
}
VStack {
if theObj.errorMsg != nil {
VStack {
Spacer()
HStack {
Spacer()
Text(theObj.errorMsg!)
.font(.footnote)
.onTapGesture {
print("theObj.errorMsg! = \(theObj.errorMsg!)")
}
Spacer()
}
Spacer()
}
.background(Color.red)
}
}
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active, .inactive:
if (MyClass.shared.errorMsg != nil) {
MyClass.shared = MyClass()
print("Error cancelled. Retry at next launch...")
}
default:
()
}
}
}
}
As I said: this doesn't work.
Very surprisingly the following variation works… I thought it couldn't work like this but it does. My big question is: WHY does it work like this? Shouldn't this be the same thing? What's the difference that I don't see?
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
var body: some View {
ZStack {
VStack {
Text("App when everything is fine")
.onTapGesture {
MyClass.shared.handleError()
}
}
ErrorMsgView()
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active, .inactive:
if (MyClass.shared.errorMsg != nil) {
MyClass.shared = MyClass()
print("Error cancelled. Retry at next launch...")
}
default:
()
}
}
}
}
struct ErrorMsgView: View {
#ObservedObject var theObj = MyClass.shared
var body: some View {
VStack {
if theObj.errorMsg != nil {
VStack {
Spacer()
HStack {
Spacer()
Text(theObj.errorMsg!)
.font(.footnote)
.onTapGesture {
print("theObj.errorMsg! = \(theObj.errorMsg!)")
}
Spacer()
}
Spacer()
}
.background(Color.red)
}
}
}
}
Also I honestly don't understand how do I conclusively kill the MyClass instance I don't need anymore? I do know how to terminate the background tasks that MyClass is running, but is it sufficient to just assign a new instance to the static var shared and the old one is purged?

SwiftUI View flickering when displaying it based on State with if-else

I want to display a certain form in the app based on a Picker selected value. However, when I switch using the segmented control fast enough (here it seems forced, but when the form is more complex it's very noticeable). Edit: this seems to be happening with all Views, not just forms.
import SwiftUI
struct ContentView: View {
#State private var calculationType = CalculationType.months
#State private var balanceOwned: String = ""
var body: some View {
VStack {
Picker("Calculation Type", selection: $calculationType) {
ForEach(CalculationType.allCases, id: \.self) {
Text($0.rawValue.capitalized)
}
}.pickerStyle(SegmentedPickerStyle())
// This check seems to be the cause of the problem!
if calculationType == .months {
CustomForm(balanceOwned: $balanceOwned)
} else if calculationType == .fixed {
CustomForm(balanceOwned: $balanceOwned)
} else {
CustomForm(balanceOwned: $balanceOwned)
}
}
}
}
struct CustomForm: View {
#Binding var balanceOwned: String
var body: some View {
Form {
Section {
TextField("Test", text: $balanceOwned)
.foregroundColor(.white)
}
}
}
}
enum CalculationType: String, CaseIterable {
case months
case fixed
case minimum
}
Whatever is inside the form flickers. How do I fix it?
You are facing structural identity problem, when dealing with conditions,
If you want to reuse same CustomForm and data can be populated based on balanceOwned, then need to preserve identity.
Either you give explicit identity or refactor structure to preserve structural identity of SwiftUI view.
Explicit Identity:
var body: some View {
....
Picker("Calculation Type", selection: $calculationType) {
ForEach(CalculationType.allCases, id: \.self) {
Text($0.rawValue.capitalized)
}
}
.onChange(of: calculationType, perform: pickerChange(_:))
.pickerStyle(SegmentedPickerStyle())
if calculationType == .months {
CustomForm(balanceOwned: $balanceOwned)
.id("11") // Common hashable explicit identity used.
} else if calculationType == .fixed {
CustomForm(balanceOwned: $balanceOwned)
.id("11")
} else {
CustomForm(balanceOwned: $balanceOwned)
.id("11")
}
...
}
Structural Identity:
var body: some View {
...
Picker("Calculation Type", selection: $calculationType) {
ForEach(CalculationType.allCases, id: \.self) {
Text($0.rawValue.capitalized)
}
}
.onChange(of: calculationType, perform: pickerChange(_:))
.pickerStyle(SegmentedPickerStyle())
CustomForm(balanceOwned: $balanceOwned) // Preserve structural identity
...
}
func pickerChange(_ type: CalculationType) {
switch type {
case .months:
balanceOwned = "Months"
case .fixed:
balanceOwned = "Fixed"
case .minimum:
balanceOwned = "Minimum"
}
}

How to create a custom TabView with NavigationView in SwiftUI?

[EDIT] - This question has been edited and simplified.
I need to create a CustomLooking TabView instead of the default one.
Here is my full code with the problem. Just run the code below.
import SwiftUI
enum TabName {
case explore, network
}
struct ContentView: View {
#State var displayedTab: TabName = .explore
var body: some View {
VStack(spacing: 0) {
Spacer()
switch displayedTab {
case .explore: AViewWhichNavigates(title: "Explore").background(Color.yellow)
case .network: AViewWhichNavigates(title: "Network").background(Color.green)
}
Spacer()
CustomTabView(displayedTab: $displayedTab)
}
}
}
struct CustomTabView: View {
#Binding var displayedTab: TabName
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
let title: String
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation")) {
Text("You are at root. Tap to navigate").navigationTitle(title)
}
})
}
}
On tab#1 click the navigation. Switch to tab#2, then Switch back to tab#1. You will see that tab#1 has popped to root.
How do I prevent the customTabView from popping to root every time i switch tabs?
All you need is a ZStack with opacity.
import SwiftUI
enum TabName {
case explore, network
}
struct ContentView: View {
#State var displayedTab: TabName = .explore
var body: some View {
VStack {
ZStack {
AViewWhichNavigates(title: "Explore")
.background(Color.green)
.opacity(displayedTab == .explore ? 1 : 0)
AViewWhichNavigates(title: "Network")
.background(Color.green)
.opacity(displayedTab == .network ? 1 : 0)
}
CustomTabView(displayedTab: $displayedTab)
}
}
}
struct CustomTabView: View {
#Binding var displayedTab: TabName
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
let title: String
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation")) {
Text("You are at root. Tap to navigate").navigationTitle(title)
}
})
}
}
The problem is that the Navigation isActive state is not recorded as well as the displayed tab state.
By recording the state of the navigation of each tab as well as which tab is active the correct navigation state can be show for each tab.
The model can be improved to remove the tuple and make it more flexible but the key thing is the use of getter and setter to use an encapsulated model of what the navigation state is for each tab in order to allow the NavigationLink to update it via a binding.
I have simplified the top level VStack and removed the top level switch as its not needed here, but it can be added back for using different types of views at the top level in a real implementation
enum TabName : String {
case Explore, Network
}
struct ContentView: View {
#State var model = TabModel()
init(){
UINavigationBar.setAnimationsEnabled(false)
}
var body: some View {
VStack(spacing: 0) {
Spacer()
AViewWhichNavigates(model: $model).background(Color.green)
Spacer()
CustomTabView(model:$model)
}
}
}
struct CustomTabView: View {
#Binding var model: TabModel
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
#Binding var model:TabModel
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation in \(model.selectedTab.rawValue)"), isActive: $model.isActive) {
Text("You are at root of \(model.selectedTab.rawValue). Tap to navigate").navigationTitle(model.selectedTab.rawValue)
}.onDisappear {
UINavigationBar.setAnimationsEnabled(model.isActive)
}
})
}
}
struct TabModel {
var selectedTab:TabName = .Explore
var isActive : Bool {
get {
switch selectedTab {
case .Explore : return tabMap.0
case .Network : return tabMap.1
}
}
set {
switch selectedTab {
case .Explore : nOn(isActive, newValue); tabMap.0 = newValue;
case .Network : nOn(isActive, newValue); tabMap.1 = newValue;
}
}
}
//tuple used to represent a fixed set of tab isActive navigation states
var tabMap = (false, false)
func nOn(_ old:Bool,_ new:Bool ){
UINavigationBar.setAnimationsEnabled(new && !old)
}
}
I think it is possible even with your custom tab view, because the issue is in rebuilding ExploreTab() when you switch tabs, so all content of that tab is rebuilt as well, so internal NavigationView on rebuilt is on first page.
Assuming you have only one ExploreTab in your app (as should be obvious), the possible solution is to make it Equatable explicitly and do not allow SwiftUI to replace it on refresh.
So
struct ExploreTab: View, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return true // prevent replacing ever !!
}
var body: some View {
// ... your code here
}
}
and
VStack(spacing: 0) {
switch displayedTab {
case .explore: ExploreTab().equatable() // << here !!
case .network: NetworkTab()
}
CustomTabView(displayedTab: $displayedTab) //This is the Custom TabBar
}
Update: tested with Xcode 12 / iOS 14 - works as described above (actually the same idea works for standard containers)
Here is a quick demo replication of CustomTabView with test environment as described above.
Full module code:
struct ExploreTab: View, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return true // prevent replacing ever !!
}
var body: some View {
NavigationView {
NavigationLink("Go", destination: Text("Explore"))
}
}
}
enum TestTabs {
case explore
case network
}
struct CustomTabView: View {
#Binding var displayedTab: TestTabs
var body: some View {
HStack {
Button("Explore") { displayedTab = .explore }
Divider()
Button("Network") { displayedTab = .network }
}
.frame(maxWidth: .infinity)
.frame(height: 80).background(Color.yellow)
}
}
struct TestCustomTabView: View {
#State private var displayedTab = TestTabs.explore
var body: some View {
VStack(spacing: 0) {
switch displayedTab {
case .explore: ExploreTab().equatable() // << here !!
case .network: Text("NetworkTab").frame(maxWidth: .infinity, maxHeight: .infinity)
}
CustomTabView(displayedTab: $displayedTab) //This is the Custom TabBar
}
.edgesIgnoringSafeArea(.bottom)
}
}