Picker in NavigationView: No refresh when custom binding changes - swift

I'm trying to save an enum value to UserDefaults. Therefor I created a custom binding. The UserDefault part works fine but unfortunately the value in the NavigationView does not get refreshed after changing the value with the Picker.
How can I get the chosen weather condition to be displayed?
PS: If not creating additional complexity, I would like to keep the enum Weather of type Int.
import SwiftUI
enum Weather: Int, CaseIterable {
case rain
case sun
case clouds
}
func getWeatherText(weather: Weather) -> String {
switch weather {
case .rain: return "Rain"
case .sun: return "Sun"
case .clouds: return "Clouds"
}
}
struct ContentView: View {
let currentWeather = Binding<Weather>(
get: {
(Weather(rawValue: UserDefaults.standard.integer(forKey: "weather")) ?? .sun)
},
set: {
UserDefaults.standard.set($0.rawValue, forKey: "weather")
}
)
let weathers = Weather.allCases
var body: some View {
NavigationView{
Form{
Picker("Weather", selection: currentWeather) {
ForEach(weathers, id: \.self) { w in
Text(getWeatherText(weather: w)).tag(w)
}
}
}
}
}
}

Thanks to the comment of Asperi, the solution is simple: replacing my custom Binding with #AppStorage. This is the result:
import SwiftUI
enum Weather: Int, CaseIterable {
case rain
case sun
case clouds
}
func getWeatherText(weather: Weather) -> String {
switch weather {
case .rain: return "Rain"
case .sun: return "Sun"
case .clouds: return "Clouds"
}
}
struct ContentView: View {
#AppStorage("weather") var currentWeather: Weather = Weather.clouds
let weathers = Weather.allCases
var body: some View {
NavigationView{
Form{
Picker("Weather", selection: $currentWeather) {
ForEach(weathers, id: \.self) { w in
Text(getWeatherText(weather: w)).tag(w)
}
}
}
}
}
}

Related

How to get value (not array) from API in SwiftUI

I work with this API: https://api.spacexdata.com/v4/rockets.
By some exploring I decided to use this API getter:
let rocketsData: [Rocket] = [] //<-- Spoiler: This is my problem
class Api {
func getRockets(completion: #escaping ([Rocket]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let rockets = try JSONDecoder().decode([Rocket].self, from: data!)
DispatchQueue.main.async {
completion(rockets)
}
} catch {
print(error.localizedDescription)
}
}
.resume()
}
}
This is my Model:
struct StageInfo: Codable {
let engines: Int
let fuelAmountTons: Double
let burnTimeSec: Int?
enum CodingKeys: String, CodingKey {
case engines
case fuelAmountTons = "fuel_amount_tons"
case burnTimeSec = "burn_time_sec"
}
}
struct Rocket: Codable, Identifiable {
let id = UUID()
let name: String
let firstFlight: String
let country: String
let costPerLaunch: Int
let firstStage: StageInfo
let secondStage: StageInfo
enum CodingKeys: String, CodingKey {
case id
case name
case firstFlight = "first_flight"
case country
case costPerLaunch = "cost_per_launch"
case firstStage = "first_stage"
case secondStage = "second_stage"
}
}
By this model I am able to get an array of values, and I can use this array in my View with only Lists or ForEach loops. But what if I want to use not the array, but some values?
In this view for example I use ForEach loop, that's why everything works perfect:
struct ContentView: View {
#State var rockets = [Rocket]() // <-- Here is the array I was talking about
var body: some View {
NavigationView {
List {
ForEach(rockets) { item in // <-- ForEach loop to get values from the array
NavigationLink(destination: RocketDetailView(rocket: item)) {
RocketRowView(rocket: item)
.padding(.vertical, 4)
}
}
} //: LIST
.navigationTitle("Rockets")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isShowingSettings = true
}) {
Image(systemName: "slider.horizontal.3")
} //: BUTTON
.sheet(isPresented: $isShowingSettings) {
SettingsView()
}
}
} //: TOOLBAR
} //: NAVIGATION
.onAppear() {
Api().getRockets { rockets in // <-- Method to get the array from the API
self.rockets = rockets
}
}
}
}
//MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And finally here I have a View with an item that I use to create the List of this items in another View:
struct RocketRowView: View {
var rocket: Rocket // <-- Here I don't need to use an array as previously, so I create a usual variable with Rocket instance
var body: some View {
HStack {
Image("Falcon-9")
.resizable()
.scaledToFill()
.frame(width: 80, height: 80, alignment: .center)
.background(Color.teal)
.cornerRadius(8)
VStack(alignment: .leading, spacing: 5) {
Text(rocket.name) // <-- So this surely doesn't work
.font(.title2)
.fontWeight(.bold)
Text(rocket.country) // <-- And this doesn't work neither
.font(.caption)
.foregroundColor(Color.secondary)
}
} //: HSTACK <-- And I can't use here inside onAppear the method that I used before to get the array, because I don't need the array
}
}
struct RocketRowView_Previews: PreviewProvider {
static var previews: some View {
RocketRowView(rocket: rocketsData[0]) // <-- Because of the variable at the top of this View I have to add missing argument for parameter 'rocket' in call. Immediately after that I get an error: "App crashed due to an out of range index"
}
}
As I wrote in the comments above I create a variable rocket of type Rocket in my RocketRowView() and because of that I have to add missing argument for parameter 'rocket' in RocketRowView_Previews. Here I get an error: "App crashed due to an out of range index". I don't understand why let rocketsData: [Rocket] = [] is empty and how I can use the rocket variable in my View without ForEach looping.
Plead help me dear experts. I've already broken my brain trying to figure out for a whole week, what I should do.
The data in the preview area has nothing to do with the received data. If you want a preview you have to provide a custom instance.
A usual way is to add a static example property to the structs like this
struct StageInfo: Codable {
let engines: Int
let fuelAmountTons: Double
let burnTimeSec: Int?
enum CodingKeys: String, CodingKey {
case engines
case fuelAmountTons = "fuel_amount_tons"
case burnTimeSec = "burn_time_sec"
}
static let firstStage = StageInfo(engines: 1, fuelAmountTons: 44.3, burnTimeSec: 169)
static let secondStage = StageInfo(engines: 1, fuelAmountTons: 3.30, burnTimeSec: 378)
}
struct Rocket: Codable, Identifiable {
let id = UUID()
let name: String
let firstFlight: String
let country: String
let costPerLaunch: Int
let firstStage: StageInfo
let secondStage: StageInfo
enum CodingKeys: String, CodingKey {
case id
case name
case firstFlight = "first_flight"
case country
case costPerLaunch = "cost_per_launch"
case firstStage = "first_stage"
case secondStage = "second_stage"
}
static let example = Rocket(id: UUID(), name: "Falcon 1", firstFlight: "2006-03-24", country: "Republic of the Marshall Islands", costPerLaunch: 6700000, firstStage: StageInfo.firstStage, secondStage: StageInfo.secondStage)
}
Then just refer to the example
struct RocketRowView_Previews: PreviewProvider {
static var previews: some View {
RocketRowView(rocket: Rocket.example)
}
}

SwiftUI Picker not changing selection value

The following picker isn't updating $selection. Regardless of what the Picker shows while running the app, selection.rawValue always returns 0.
What is preventing the Picker from updating the State variable selection?
import SwiftUI
struct OrderPicker: View {
let initialIndex: Int
#State private var selection: Index
enum Index: Int, CaseIterable, Identifiable {
case none = 0,
first = 1,
second = 2,
third = 3
var id: Self { self }
}
init(initialIndex: Int) {
self.initialIndex = initialIndex
_selection = State(initialValue: OrderPicker.Index(rawValue: initialIndex) ?? .none)
}
var body: some View {
Form {
Picker("Order in list", selection: $selection) {
ForEach(Index.allCases) { index in
Text(String(describing: index)).tag(index)
}
}
}
.frame(height: 116)
}
func getOrderIndex() -> Int {
let index = selection.rawValue
return index
}
}
Here is an approach for you:
struct ContentView: View {
#State private var selection: PickerType = PickerType.none
var body: some View {
OrderPicker(selection: $selection)
}
}
struct OrderPicker: View {
#Binding var selection: PickerType
var body: some View {
Form {
Picker("Order in list", selection: $selection) {
ForEach(PickerType.allCases, id: \.self) { item in
Text(item.rawValue)
}
}
.pickerStyle(WheelPickerStyle())
.onChange(of: selection, perform: { newValue in
print(newValue.rawValue, newValue.intValue)
})
}
}
}
enum PickerType: String, CaseIterable {
case none, first, second, third
var intValue: Int {
switch self {
case .none: return 0
case .first: return 1
case .second: return 2
case .third: return 3
}
}
}

Associated enum state in SwiftUI

How can I use an associated enum as a #State variable in an if statement in SwiftUI?
struct ProfileView: View {
#State private var choice = Choice.simple
private enum Choice {
case simple
case associated(Int)
}
var body: some View {
if choice == .simple {
Text("Simple")
}
}
}
The compiler reports this error:
Protocol 'Equatable' requires that 'ProfileView.Choice' conform to 'Equatable'
Here is fixed variant. Tested with Xcode 11.4.
struct ProfileView: View {
#State private var choice = Choice.simple
private enum Choice: Equatable {
case simple
case associated(Int)
}
var body: some View {
Group {
if choice == .simple {
Text("Simple")
} else {
Text("Other")
}
}
}
}
You need to use if case to check if an enum variable matches a certain case.
var body: some View {
if case .simple = choice {
return Text("Simple")
} else {
return Text("Not so simple")
}
}
If you actually want to use the associated value to be displayed, I'd suggest using a switch to cover all enum cases.
var body: some View {
let text: String
switch choice {
case .simple:
text = "Simple"
case .associated(let value):
text = "\(value)"
}
return Text(text)
}

What is the correct way to create SwiftUI Binding with array of associative enum?

I got some (unexplained) crashes earlier today, and simplified my code to what is seen below. The crashing went away, but I am not 100% sure. Is the code below the correct way to create Binding on an array of enums? And if yes, can this code be made simpler?
import SwiftUI
enum TheEnum: Hashable {
case one(Int), two(Float)
}
class TestModel : ObservableObject {
#Published var enumArray = [TheEnum.one(5), TheEnum.two(6.0)]
}
struct ContentView: View {
#ObservedObject var testModel = TestModel()
var body: some View {
HStack {
ForEach(testModel.enumArray, id: \.self) { value -> AnyView in
switch value {
case .one(var intVal):
let b = Binding(get: {
intVal
}) {
intVal = $0
}
return AnyView(IntView(intVal: b))
case .two(var floatVal):
let b = Binding(get: {
floatVal
}) {
floatVal = $0
}
return AnyView(FloatView(floatVal: b))
}
}
}
}
}
struct IntView: View {
#Binding var intVal: Int
var body: some View {
Text("\(intVal)")
}
}
struct FloatView: View {
#Binding var floatVal: Float
var body: some View {
Text("\(floatVal)")
}
}

Alternative to switch statement in SwiftUI ViewBuilder block?

⚠️ 23 June 2020 Edit: From Xcode 12, both switch and if let statements will be supported in the ViewBuilder!
I’ve been trying to replicate an app of mine using SwiftUI. It has a RootViewController which, depending on an enum value, shows a different child view controller. As in SwiftUI we use views instead of view controllers, my code looks like this:
struct RootView : View {
#State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
Unfortunately, I get a warning:
Closure containing control flow statement cannot be used with function builder ViewBuilder.
So, are there any alternatives to switch so I can replicate this behaviour?
⚠️ 23 June 2020 Edit: From Xcode 12, both switch and if let statements will be supported in the ViewBuilder!
Thanks for the answers, guys. I’ve found a solution on Apple’s Dev Forums.
It’s answered by Kiel Gillard. The solution is to extract the switch in a function as Lu_, Linus and Mo suggested, but we have to wrap the views in AnyView for it to work – like this:
struct RootView: View {
#State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> AnyView {
switch containedViewType {
case .home: return AnyView(HomeView())
case .categories: return AnyView(CategoriesView())
...
}
}
Update: SwiftUI 2 now includes support for switch statements in function builders, https://github.com/apple/swift/pull/30174
Adding to Nikolai's answer, which got the switch compiling but not working with transitions, here's a version of his example that does support transitions.
struct RootView: View {
#State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> some View {
switch containedViewType {
case .home: return AnyView(HomeView()).id("HomeView")
case .categories: return AnyView(CategoriesView()).id("CategoriesView")
...
}
}
Note the id(...) that has been added to each AnyView. This allows SwiftUI to identify the view within it's view hierarchy allowing it to apply the transition animations correctly.
It looks like you don't need to extract the switch statement into a separate function if you specify the return type of a ViewBuilder. For example:
Group { () -> Text in
switch status {
case .on:
return Text("On")
case .off:
return Text("Off")
}
}
Note: You can also return arbitrary view types if you wrap them in AnyView and specify that as the return type.
You must wrap your code in a View, such as VStack, or Group:
var body: some View {
Group {
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
or, adding return values should work:
var body: some View {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
The best-practice way to solve this issue, however, would be to create a method that returns a view:
func nextView(for containedView: YourViewEnum) -> some AnyView {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
var body: some View {
nextView(for: containedView)
}
You can use enum with #ViewBuilder as follow ...
Declear enum
enum Destination: CaseIterable, Identifiable {
case restaurants
case profile
var id: String { return title }
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .profile: return "Profile"
}
}
}
Now in the View file
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
view(for: selectedDestination)
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
If you want to use the same case with the NavigationLink ... You can use it as follow
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
List(Destination.allCases,
selection: $selectedDestination) { item in
NavigationLink(destination: view(for: selectedDestination),
tag: item,
selection: $selectedDestination) {
Text(item.title).tag(item)
}
}
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
Providing default statement in the switch solved it for me:
struct RootView : View {
#State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
default: EmptyView()
}
}
}
You can do with a wrapper View
struct MakeView: View {
let make: () -> AnyView
var body: some View {
make()
}
}
struct UseMakeView: View {
let animal: Animal = .cat
var body: some View {
MakeView {
switch self.animal {
case .cat:
return Text("cat").erase()
case .dog:
return Text("dog").erase()
case .mouse:
return Text("mouse").erase()
}
}
}
}
For not using AnyView(). I will use a bunch of if statements and implement the protocols Equatable and CustomStringConvertible in my Enum for retrieving my associated values:
var body: some View {
ZStack {
Color("background1")
.edgesIgnoringSafeArea(.all)
.onAppear { self.viewModel.send(event: .onAppear) }
// You can use viewModel.state == .loading as well if your don't have
// associated values
if viewModel.state.description == "loading" {
LoadingContentView()
} else if viewModel.state.description == "idle" {
IdleContentView()
} else if viewModel.state.description == "loaded" {
LoadedContentView(list: viewModel.state.value as! [AnimeItem])
} else if viewModel.state.description == "error" {
ErrorContentView(error: viewModel.state.value as! Error)
}
}
}
And I will separate my views using a struct:
struct ErrorContentView: View {
var error: Error
var body: some View {
VStack {
Image("error")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Text(error.localizedDescription)
}
}
}