In order to manage the visual state of my SwiftUI view, I have an ObservedObject that contains a State enum. Whenever I receive remote data, I use the enum with an associated value so that I can display the content I received:
final class ViewModel: ObservableObject {
#Published private(set) var state: State = .idle
func doWork() {
// Change the state to .loading so that we can display a loader in our view
state = .loading
let values = doSomeRemoteWork { [weak self] values in
// work is done, we can display the result
self?.state = .loaded(values)
}
}
enum State {
case idle
case loading
case loaded([Value])
case error(any Error)
}
}
// Value is just a struct with some basic data inside
struct Value: Identifiable {
let id: String
let someProperty: String
let anotherProperty: Double
}
// swiftui view
struct ContentView: some View {
#ObservedObject private var vm: ViewModel
init(vm: ViewModel) {
self.vm = vm
}
var body: some View {
switch vm.state {
case .idle:
Button("Get values") {
vm.doWork()
}
case .loading: Text("loading...")
case .loaded(let values):
Text("got \(values.count) values")
.onTapGesture { refresh() }
case .error(let error): Text("got an error : \(error.localizedDescription)")
}
}
private func refresh() {
vm.doWork()
}
}
This works fine but I implemented a refresh that basically just invokes the doWork() method again. When I do so, I can see that the memory usage of my app goes up and never goes down but I can't seem to understand why.
From what I understand, an enum will strong retain its associated values but shouldn't setting the state to .loading clear the enum associated value ? Is there a way to do so ?
I am trying to control the flow of my app when it first appears based on an async call to see if the user is logged in or not.
If this call is synchronous I can do a simple switch or if else to determine the view to show.
But I don't know how to handle the flow based on an async request. So far this is what I have done but changing the state var does not change the view which is displayed. The following is in the app file #main function:
enum LoginStatus {
case undetermined
case signedIn
case signedOut
}
#main
struct SignUpApp: App {
#State var loginStatus: LoginStatus = .undetermined
public init() {
getLoginStatus()
}
var body: some Scene {
WindowGroup {
switch loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
DispatchQueue.main.async {
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
}
I imagine this is a common task and wondering what the best way to handle it is.
You have the right idea, but generally with async calls, you're probably going to want to move them into an ObservableObject rather than have them in your View code itself, since views are transient in SwiftUI (although the exception may be the top level App like you have).
class SignInManager : ObservableObject {
#Published var loginStatus: LoginStatus = .undetermined
public init() {
getLoginStatus()
}
func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
//may need to use DispatchQueue.main.async {} to make sure you're on the main thread
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
#main
struct SignUpApp: App {
#StateObject var signInManager = SignInManager()
var body: some Scene {
WindowGroup {
switch signInManager.loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
}
enum LoginStatus {
case undetermined
case signedIn
case signedOut
}
#main
struct SignUpApp: App {
#State var loginStatus: LoginStatus = .undetermined
public init() {
getLoginStatus()
}
var body: some Scene {
WindowGroup {
switch loginStatus {
case .undetermined:
Text("SigningIn")
case .signedIn:
EnterAppView()
case .signedOut:
LoginView()
}
}
}
func getLoginStatus() {
someAsyncFunctionToGetSignInStatus { result in
DispatchQueue.main.async {
switch result {
case .success:
self.loginStatus = .signedIn
case .failure:
self.loginStatus = .signedOut
}
}
}
}
}
It's probably a stupid question but I'm trying to allow all string enums as type for a variable in a Struct. The code below is completely wrong but I hope that it will make my question clearer.
My idea was to conform all enums to the same protocol but I can't access .allCases with this approach.
My goal is that I can pass any string enum to the ListView which will then display all components of the enum (here: one; two; three).
Does anybody have an idea how to do this? It must be a very basic Swift thing but I wasn't able to figure it out searching through the web. Thank you so much!
import SwiftUI
struct ContentView: View {
var body: some View {
ListView(myEnum: Elements.self) //error here as well
}
}
protocol StringRepresentable {
var rawValue: String { get }
}
enum Elements: String, Equatable, CaseIterable, StringRepresentable {
case one
case two
case three
}
struct ListView: View {
let myEnum: StringRepresentable //doesn't work
var body: some View {
ForEach(myEnum.allCases, id: \.self) { elem in //can't access .allCases
Text(elem.rawValue)
}
}
}
There's several errors in your original code. First you weren't using a VStack (or List or LazyVStack) so your foreach would only render one element.
Secondly, you were passing the type, not the elements itself to the list view. And finally, your own StringRepresentable protocol is unnecessary, you can use RawRepresentable with it's associated type RawValue constrained to String
i.e. something like this:
struct ContentView: View {
var body: some View {
VStack {
ListView(values: Fruits.allCases)
ListView(values: Animals.allCases)
}
}
}
enum Fruits: String, CaseIterable {
case apple
case orange
case banana
}
enum Animals: String, CaseIterable {
case cat
case dog
case elephant
}
struct ListView<T: RawRepresentable & Hashable>: View where T.RawValue == String {
let values: [T]
var body: some View {
LazyVStack {
ForEach(values, id: \.self) { elem in
Text(elem.rawValue)
}
}
}
}
Which renders like this
Here is a variant that will work by sending in all items of an enum to the view rather that the enum itself and making the view generic.
struct ContentView: View {
var body: some View {
ListView(myEnum: Elements.allCases)
}
}
protocol StringRepresentable {
var rawValue: String { get }
}
enum Elements: String, Equatable, CaseIterable, StringRepresentable {
case one
case two
case three
}
struct ListView<T: CaseIterable & StringRepresentable>: View {
let myEnum: [T]
#State private var selectedValue: String = ""
var body: some View {
ForEach(0..<myEnum.count) { index in
Text(myEnum[index].rawValue)
.onTapGesture {
selectedValue = myEnum[index].rawValue
}
}
}
}
What are proven approaches for structuring the networking layer of a SwiftUI app? Specifically, how do you structure using URLSession to load JSON data to be displayed in SwiftUI Views and handling all the different states that can occur properly?
Here is what I came up with in my last projects:
Represent the loading process as a ObservableObject model class
Use URLSession.dataTaskPublisher for loading
Using Codable and JSONDecoder to decode the response to Swift types using the Combine support for decoding
Keep track of the state in the model as a #Published property so that the view can show loading/error states.
Keep track of the loaded results as a #Published property in a separate property for easy usage in SwiftUI (you could also use View#onReceive to subscribe to the publisher directly in SwiftUI but keeping the publisher encapsulated in the model class seemed more clean overall)
Use the SwiftUI .onAppear modifier to trigger the loading if not loaded yet.
Using the .overlay modifier is convenient to show a Progress/Error view depending on the state
Extract reusable components for repeatedly occuring tasks (here is an example: EndpointModel)
Standalone example code for that approach (also available in my SwiftUIPlayground):
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Combine
import SwiftUI
struct TypiTodo: Codable, Identifiable {
var id: Int
var title: String
}
class TodosModel: ObservableObject {
#Published var todos = [TypiTodo]()
#Published var state = State.ready
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/")!
let urlSession = URLSession.shared
var dataTask: AnyPublisher<[TypiTodo], Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: [TypiTodo].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load() {
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case let .failure(error):
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.todos = value
}
))
}
func loadIfNeeded() {
assert(Thread.isMainThread)
guard case .ready = self.state else { return }
self.load()
}
}
struct TodosURLSessionExampleView: View {
#ObservedObject var model = TodosModel()
var body: some View {
List(model.todos) { todo in
Text(todo.title)
}
.overlay(StatusOverlay(model: model))
.onAppear { self.model.loadIfNeeded() }
}
}
struct StatusOverlay: View {
#ObservedObject var model: TodosModel
var body: some View {
switch model.state {
case .ready:
return AnyView(EmptyView())
case .loading:
return AnyView(ActivityIndicatorView(isAnimating: .constant(true), style: .large))
case .loaded:
return AnyView(EmptyView())
case let .error(error):
return AnyView(
VStack(spacing: 10) {
Text(error.localizedDescription)
.frame(maxWidth: 300)
Button("Retry") {
self.model.load()
}
}
.padding()
.background(Color.yellow)
)
}
}
}
struct TodosURLSessionExampleView_Previews: PreviewProvider {
static var previews: some View {
Group {
TodosURLSessionExampleView(model: TodosModel())
TodosURLSessionExampleView(model: self.exampleLoadedModel)
TodosURLSessionExampleView(model: self.exampleLoadingModel)
TodosURLSessionExampleView(model: self.exampleErrorModel)
}
}
static var exampleLoadedModel: TodosModel {
let todosModel = TodosModel()
todosModel.todos = [TypiTodo(id: 1, title: "Drink water"), TypiTodo(id: 2, title: "Enjoy the sun")]
todosModel.state = .loaded
return todosModel
}
static var exampleLoadingModel: TodosModel {
let todosModel = TodosModel()
todosModel.state = .loading(ExampleCancellable())
return todosModel
}
static var exampleErrorModel: TodosModel {
let todosModel = TodosModel()
todosModel.state = .error(ExampleError.exampleError)
return todosModel
}
enum ExampleError: Error {
case exampleError
}
struct ExampleCancellable: Cancellable {
func cancel() {}
}
}
Splitting off the state / data / networking into a separate #ObservableObject class outside the View Struct is definitely the way to go. There are too many SwiftUI "Hello World" examples out there stuffing it all into the View struct.
As a best practice you could look to standardize your #ObservableObject naming inline with MVVM and call that "Model" class a ViewModel, as in:
#StateObject var viewModel = TodosViewModel()
The majority of code in there is handling overlay state, onAppear events and display issues for the View.
Create a new TodosModel class and reference that in the ViewModel:
#ObservedObject var model = TodosModel()
Then move all the networking / api / JSON code into that class with one method called by ViewModel:
public func getList() -> AnyPublisher<[TypiTodo], Error>
The View-ViewModel-Model are now split up, related to Paul D's comment, the ViewModel could combine 1 or more Models to return whatever the view needs. And, more importantly, the TodoModel entity knows nothing about the View and can focus on http / JSON / CRUD.
Below is great example using Combine / HTTP / JSON decode. You can see how it uses tryMap, mapError to further separate the networking from the decode errors. https://gist.github.com/stinger/e8b706ab846a098783d68e5c3a4f0ea5
See a very short and clear explanation of the difference between #StateObject and #ObservedObject in this article:
https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9
I can do a static List like
List {
View1()
View2()
}
But how do i make a dynamic list of elements from an array?
I tried the following but got error: Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
let elements: [Any] = [View1.self, View2.self]
List {
ForEach(0..<elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
Is there any work around for this?
What I am trying to accomplish is a List contaning dynamic set of elements that are not statically entered.
Looks like the answer was related to wrapping my view inside of AnyView
struct ContentView : View {
var myTypes: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<myTypes.count) { index in
self.buildView(types: self.myTypes, index: index)
}
}
}
func buildView(types: [Any], index: Int) -> AnyView {
switch types[index].self {
case is View1.Type: return AnyView( View1() )
case is View2.Type: return AnyView( View2() )
default: return AnyView(EmptyView())
}
}
}
With this, i can now get view-data from a server and compose them. Also, they are only instanced when needed.
if/let flow control statement cannot be used in a #ViewBuilder block.
Flow control statements inside those special blocks are translated to structs.
e.g.
if (someBool) {
View1()
} else {
View2()
}
is translated to a ConditionalValue<View1, View2>.
Not all flow control statements are available inside those blocks, i.e. switch, but this may change in the future.
More about this in the function builder evolution proposal.
In your specific example you can rewrite the code as follows:
struct ContentView : View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<elements.count) { index in
if self.elements[index] is View1 {
View1()
} else {
View2()
}
}
}
}
}
You can use dynamic list of subviews, but you need to be careful with the types and the instantiation. For reference, this is a demo a dynamic 'hamburger' here, github/swiftui_hamburger.
// Pages View to select current page
/// This could be refactored into the top level
struct Pages: View {
#Binding var currentPage: Int
var pageArray: [AnyView]
var body: AnyView {
return pageArray[currentPage]
}
}
// Top Level View
/// Create two sub-views which, critially, need to be cast to AnyView() structs
/// Pages View then dynamically presents the subviews, based on currentPage state
struct ContentView: View {
#State var currentPage: Int = 0
let page0 = AnyView(
NavigationView {
VStack {
Text("Page Menu").color(.black)
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Page"), displayMode: .large)
}
}
)
let page1 = AnyView(
NavigationView {
VStack {
Text("Another Page Menu").color(.black)
List(["A", "B", "C", "D", "E"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Second Page"), displayMode: .large)
}
}
)
var body: some View {
let pageArray: [AnyView] = [page0, page1]
return Pages(currentPage: self.$currentPage, pageArray: pageArray)
}
}
You can do this by polymorphism:
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
class ViewBase: Identifiable {
func showView() -> AnyView {
AnyView(EmptyView())
}
}
class AnyView1: ViewBase {
override func showView() -> AnyView {
AnyView(View1())
}
}
class AnyView2: ViewBase {
override func showView() -> AnyView {
AnyView(View2())
}
}
struct ContentView: View {
let views: [ViewBase] = [
AnyView1(),
AnyView2()
]
var body: some View {
List(self.views) { view in
view.showView()
}
}
}
I found a little easier way than the answers above.
Create your custom view.
Make sure that your view is Identifiable
(It tells SwiftUI it can distinguish between views inside the ForEach by looking at their id property)
For example, lets say you are just adding images to a HStack, you could create a custom SwiftUI View like:
struct MyImageView: View, Identifiable {
// Conform to Identifiable:
var id = UUID()
// Name of the image:
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.frame(width: 50, height: 50)
}
}
Then in your HStack:
// Images:
HStack(spacing: 10) {
ForEach(images, id: \.self) { imageName in
MyImageView(imageName: imageName)
}
Spacer()
}
SwiftUI 2
You can now use control flow statements directly in #ViewBuilder blocks, which means the following code is perfectly valid:
struct ContentView: View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0 ..< elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
}
}
SwiftUI 1
In addition to the accepted answer you can use #ViewBuilder and avoid AnyView completely:
#ViewBuilder
func buildView(types: [Any], index: Int) -> some View {
switch types[index].self {
case is View1.Type: View1()
case is View2.Type: View2()
default: EmptyView()
}
}
Is it possible to return different Views based on needs?
In short: Sort of
As it's fully described in swift.org, It is IMPOSSIIBLE to have multiple Types returning as opaque type
If a function with an opaque return type returns from multiple places, all of the possible return values must have the same type. For a generic function, that return type can use the function’s generic type parameters, but it must still be a single type.
So how List can do that when statically passed some different views?
List is not returning different types, it returns EmptyView filled with some content view. The builder is able to build a wrapper around any type of view you pass to it, but when you use more and more views, it's not even going to compile at all! (try to pass more than 10 views for example and see what happens)
As you can see, List contents are some kind of ListCoreCellHost containing a subset of views that proves it's just a container of what it represents.
What if I have a lot of data, (like contacts) and want to fill a list for that?
You can conform to Identifiable or use identified(by:) function as described here.
What if any contact could have a different view?
As you call them contact, it means they are same thing! You should consider OOP to make them same and use inheritance advantages. But unlike UIKit, the SwiftUI is based on structs. They can not inherit each other.
So what is the solution?
You MUST wrap all kind of views you want to display into the single View type. The documentation for EmptyView is not enough to take advantage of that (for now). BUT!!! luckily, you can use UIKit
How can I take advantage of UIKit for this
Implement View1 and View2 on top of UIKit.
Define a ContainerView with of UIKit.
Implement the ContainerView the way that takes argument and represent View1 or View2 and size to fit.
Conform to UIViewRepresentable and implement it's requirements.
Make your SwiftUI List to show a list of ContainerView
So now it's a single type that can represent multiple views
Swift 5
this seems to work for me.
struct AMZ1: View {
var body: some View {
Text("Text")
}
}
struct PageView: View {
let elements: [Any] = [AMZ1(), AMZ2(), AMZ3()]
var body: some View {
TabView {
ForEach(0..<elements.count) { index in
if self.elements[index] is AMZ1 {
AMZ1()
} else if self.elements[index] is AMZ2 {
AMZ2()
} else {
AMZ3()
}
}
}
import SwiftUI
struct ContentView: View {
var animationList: [Any] = [
AnimationDemo.self, WithAnimationDemo.self, TransitionDemo.self
]
var body: some View {
NavigationView {
List {
ForEach(0..<animationList.count) { index in
NavigationLink(
destination: animationIndex(types: animationList, index: index),
label: {
listTitle(index: index)
})
}
}
.navigationBarTitle("Animations")
}
}
#ViewBuilder
func listTitle(index: Int) -> some View {
switch index {
case 0:
Text("AnimationDemo").font(.title2).bold()
case 1:
Text("WithAnimationDemo").font(.title2).bold()
case 2:
Text("TransitionDemo").font(.title2).bold()
default:
EmptyView()
}
}
#ViewBuilder
func animationIndex(types: [Any], index: Int) -> some View {
switch types[index].self {
case is AnimationDemo.Type:
AnimationDemo()
case is WithAnimationDemo.Type:
WithAnimationDemo()
case is TransitionDemo.Type:
TransitionDemo()
default:
EmptyView()
}
}
}
enter image description here