SwiftUI: heterogeneous collection of destination views for NavigationLink? - ios

I’m building like a demo app of different examples, and I’d like the root view to be a List that can navigate to the different example views. Therefore, I tried creating a generic Example struct which can take different destinations Views, like this:
struct Example<Destination: View> {
let id: UUID
let title: String
let destination: Destination
init(title: String, destination: Destination) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: Example1View()),
Example(title: "Example 2", destination: Example2View())
]
var body: some View {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
Unfortunately, this results in an error because examples is a heterogeneous collection:
I totally understand why this is broken; I’m creating a heterogeneous array of examples because each Example struct has its own different, strongly typed destination. But I don’t know how to achieve what I want, which is an array that I can make a List out of which has a number of different allowed destinations.
I’ve run into this kind of thing in the past, and in the past I’ve gotten around it by wrapping my generic type and only exposing the exact properties I needed (e.g. if I had a generic type that had a title, I would make a wrapper struct and protocol that exposed only the title, and then made an array of that wrapper struct). But in this case NavigationLink needs to have the generic type itself, so there’s not a property I can just expose to it in a non-generic way.

You can use the type-erased wrapper AnyView. Instead of making Example generic, make the destination view inside of it be of type AnyView and wrap your views in AnyView when constructing an Example.
For example:
struct Example {
let id: UUID
let title: String
let destination: AnyView
init(title: String, destination: AnyView) {
self.id = UUID()
self.title = title
self.destination = destination
}
}
struct Example1View: View {
var body: some View {
Text("Example 1!")
}
}
struct Example2View: View {
var body: some View {
Text("Example 2!")
}
}
struct ContentView: View {
let examples = [
Example(title: "Example 1", destination: AnyView(Example1View())),
Example(title: "Example 2", destination: AnyView(Example2View()))
]
var body: some View {
NavigationView {
List(examples, id: \.id) { example in
NavigationLink(destination: example.destination) {
Text(example.title)
}
}
}
}
}

Related

SwiftUI Picker in reusable component with protocol cannot conform to Hashable

I'm trying to build a reusable component that includes a SwiftUI Picker that can work with different types in several places in my app. I created a Pickable protocol that conforms to Hashable, but when I try to use it, the Picker and the ForEach complain that Type 'any Pickable' cannot conform to 'Hashable'
import SwiftUI
struct PickerRow: View {
let title: String
let options: [any Pickable]
#State var selection: any Pickable
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subHeading)
Picker(title, selection: $selection, content: {
ForEach(options, id: \.self) {
Text($0.name)
}
}).pickerStyle(.menu)
}
}
}
protocol Pickable: Hashable {
var name: String { get }
}
Is there a way to get something like this to work without specifying a concrete type?
If you think about it, it makes sense.
What would you expect to happen if that code was valid and you used it like this?
struct ContentView: View {
let options = [PickableA(), PickableB()]
#State var selection = PickableC()
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}
That couldn't possibly work right?
What you need is a way to make sure that there is a constraint that forces options and selection to be of the same concrete type (consider Equatable for example, both String and Int are conforming, but you cannot compare them).
One possible solution would be a generic constraint in the declaration of your struct (also note the #Binding instead of #State since we modify external values):
struct PickerRow<Option: Pickable>: View {
let title: String
let options: [Option]
#Binding var selection: Option
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subheadline)
Picker(title, selection: $selection) {
ForEach(options, id: \.self) {
Text($0.name)
}
}.pickerStyle(.menu)
}
}
}
which you could use like this:
struct Person: Pickable {
let name: String
}
struct ContentView: View {
let options = [Person(name: "Bob"), Person(name: "Alice")]
#State var selection = Person(name: "Bob")
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}

Array of different Views in SwiftUI iterable in a list as NavigationLink

I am making an app with various screens, and I want to collect all the screens that all conform to View in an array of views. On the app's main screen, I want to show a list of all the screen titles with a NavigationLink to all of them.
The problem I am having is that if I try to create a custom view struct and have a property that initialises to a given screen and returns it. I keep running into issues and the compiler forces me to change the variable to accept any View rather than just View or the erased type some View.
struct Screen: View, Identifiable {
var id: String {
return title
}
let title: String
let destination: any View
var body: some View {
NavigationLink(destination: destination) { // Type 'any View' cannot conform to 'View'
Text(title)
}
}
}
This works:
struct Screen<Content: View>: View, Identifiable {
var id: String {
return title
}
let title: String
let destination: Content
var body: some View {
NavigationLink(destination: destination) {
Text(title)
}
}
}
But the issue with this approach is that I cannot put different screens into an Array which can be fixed as follows:
struct AllScreens {
var screens: [any View] = []
init(){
let testScreen = Screen(title: "Charties", destination: SwiftChartsScreen())
let testScreen2 = Screen(title: "Test", destination: NavStackScreen())
screens = [testScreen, testScreen2]
}
}
But when I try to access the screens in a List it cannot infer what view it is without typecasting. The end result I am trying to achieve is being able to pass in an array of screens and get their title displayed in a list like below.
At the moment I can only achieve this by hardcoding the lists elements, which works.
import SwiftUI
struct MainScreen: View {
let screens = AppScreens.allCases
var body: some View {
NavigationStack() {
List() {
AppScreens.chartsScreen
AppScreens.navStackScreen
}
.navigationTitle("WWDC 22")
}
}
}
SwiftUI is data-driven reactive framework and Swift is strict typed language, so instead of trying to put different View types (due to generics) into one array (requires same type), we can make data responsible for providing corresponding view (that now with help of ViewBuilder is very easy).
So here is an approach. Tested with Xcode 13+ / iOS 15+ (NavigationView or NavigationStack - it is not important)
enum AllScreens: CaseIterable, Identifiable { // << type !!
case charts, navStack // << known variants
var id: Self { self }
#ViewBuilder var view: some View { // corresponding view !!
switch self {
case .charts:
Screen(title: "Charties", destination: SwiftChartsScreen())
case .navStack:
Screen(title: "Test", destination: NavStackScreen())
}
}
}
usage is obvious:
struct MainScreen: View {
var body: some View {
NavigationStack { // or `NavigationView` for backward compatibility
List(AllScreens.allCases) {
$0.view // << data knows its presenter
}
.navigationTitle("WWDC 22")
}
}
}
Test module on GitHub

Sharing Data between Views in Swift/better approach for this?

I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄

Is there a way to create objects in swiftUI view based on a value gathered from a previous view?

I have recently started my journey into iOS development learning swift and swift UI. I keep running into issues when it comes to app architecture. The problem i am trying to solve is this: Let's say I have an app where the user first selects a number and then presses next. The user selected number is supposed to represent the number of text fields that appear on the next view. For example, if the user selects 3 then 3 text fields will appear on the next view but if the user selects 5 then 5 texts fields will appear. Is the solution to just have a view for each case? Or is there some way to dynamically add objects to a view based on the user input. Can anyone explain how they would handle a case like this?
Views can get passed parameters (including in NavigationLink) that can determine what they look like. Here's a simple example with what you described:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(numberOfFields: numberOfFields)) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView : View {
var numberOfFields : Int
var body: some View {
VStack {
ForEach(0..<numberOfFields) { index in
TextField("", text: .constant("Field \(index + 1)"))
}
}
}
}
Notice how numberOfFields is stored as #State in the parent view and then passed to the child view dynamically.
In general, it would probably be a good idea to visit some SwiftUI tutorials as this type of thing will be covered by most of them. Apple's official tutorials are here: https://developer.apple.com/tutorials/swiftui
Another very popular resource is Hacking With Swift: https://www.hackingwithswift.com/100/swiftui
Update, based on comments:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(textInputs: Array(repeating: "test", count: numberOfFields))) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct Model : Identifiable {
var id = UUID()
var text : String
}
class ViewModel : ObservableObject {
#Published var strings : [Model] = []
}
struct DetailView : View {
var textInputs : [String]
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(Array(viewModel.strings.enumerated()), id: \.1.id) { (index,text) in
TextField("", text: $viewModel.strings[index].text)
}
}.onAppear {
viewModel.strings = textInputs.map { Model(text: $0) }
}
}
}

Passing views, or higher-order components, in SwiftUI

As someone who knows React, coming to SwiftUI I'm having challenges to find the right abstractions. Here's an example, but my question is more general. It's related to passing views or, what the React community calls, higher-order components. My example is below. TLDR: how do I abstract and remove duplication in the list views below?
Some models (these will differ in the end):
struct Apple: Comparable, Identifiable {
let id: UUID = UUID()
let label: String
static func < (lhs: Apple, rhs: Apple) -> Bool {
lhs.label < rhs.label
}
}
struct Banana: Comparable, Identifiable {
let id: UUID = UUID()
let label: String
static func < (lhs: Banana, rhs: Banana) -> Bool {
lhs.label < rhs.label
}
}
Some basic detail views (these will differ in the end):
struct AppleView: View {
let apple: Apple
var body: some View {
Text(apple.label)
}
}
struct BananaView: View {
let banana: Banana
var body: some View {
Text(banana.label)
}
}
And two list views with a lot of duplication:
struct AppleListView: View {
let title: String
let apples: [Apple]
var body: some View {
List(apples.sorted()) { apple in
NavigationLink(destination: AppleView(apple: apple)) {
Text(apple.label)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}
struct BananaListView: View {
let title: String
let bananas: [Banana]
var body: some View {
List(bananas.sorted()) { banana in
NavigationLink(destination: BananaView(banana: banana))
Text(banana.label)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}
As you can see, it differs only in small parts. The type of the collection differs and the destination view. I want to remain flexible when it comes to this destination view as Apple and Banana, and their detail views above, will differ in the end. Furthermore it's likely that I want to add Cherry later on, so there's value in abstracting this list view.
So, my question is: how can I best abstract the list views above and remove the duplication in there? What would you suggest? My attempts are below, but it leaves me with type errors. It touches on the higher-order component idea, mentioned earlier.
My attempt with type errors:
struct AppleListView: View {
let title: String
let apples: [Apple]
var body: some View {
ListView(
title: title,
rows: apples, // it complains about types here -> `Cannot convert value of type '[Apple]' to expected argument type 'Array<_>'`
rowView: { apple in Text(apple.label) },
destinationView: { apple in AppleView(apple: apple) }
)
}
}
struct BananaListView: View {
let title: String
let bananas: [Banana]
var body: some View {
ListView(
title: title,
rows: bananas, // it complains about types here -> `Cannot convert value of type '[Banana]' to expected argument type 'Array<_>'`
rowView: { banana in Text(banana.label) },
destinationView: { banana in BananaView(banana: banana) }
)
}
}
struct ListView<Content: View, Row: Comparable & Identifiable>: View {
let title: String
let rows: [Row]
let rowView: (Row) -> Content
let destinationView: (Row) -> Content
var body: some View {
List(rows.sorted()) { row in
NavigationLink(destination: self.destinationView(row)) {
self.rowView(row)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}
It is because you made same type for Label and Destination, here is fixed variant
struct ListView<Target: View, Label: View, Row: Comparable & Identifiable>: View {
let title: String
let rows: [Row]
let rowView: (Row) -> Label
let destinationView: (Row) -> Target
var body: some View {
List(rows.sorted()) { row in
NavigationLink(destination: self.destinationView(row)) {
self.rowView(row)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}

Resources