Using protocol to capture SwiftUI view - swift

I’m trying to write a protocol that requires conformers to have a View property, or a #ViewBuilder method that returns some View.
I want to have a re-useable composite view that can build different sub views based on what type of data needs to be displayed.
The protocol would look like this…
protocol RowView {
var leftSide: some View { get }
var rightSide: some View { get }
}
That way I could call something like this…
struct Example: RowView {
var id: Int
var leftSide: some View { … }
var rightSide: some View { … }
}
struct ContentView: View {
let rows: [RowView]
var body: some View {
VStack {
Foreach(rows, id: \.id) {
HStack {
$0.leftSide
$0.rightSide
}
}
}
}
}

You need to change the protocol to something like:
protocol RowView {
associatedtype LView: View
associatedtype RView: View
var leftSide: LView { get }
var rightSide: RView { get }
}
Also, use the concrete Example type in the content view instead of the protocol (the protocol you defined doesn't have id at all):
let rows: [Example]
Also! you can make the RowView to be identifiable as your need, So no need for id: \.id anymore:
protocol RowView: Identifiable
A working code:
protocol RowView: Identifiable {
associatedtype LView: View
associatedtype RView: View
var leftSide: LView { get }
var rightSide: RView { get }
}
struct Example: RowView {
var id: Int
var leftSide: some View { Text("Left") }
var rightSide: some View { Text("Right") }
}
struct ContentView: View {
let rows: [Example] = [
.init(id: 1),
.init(id: 2)
]
var body: some View {
VStack {
ForEach(rows) { row in
HStack {
row.leftSide
row.rightSide
}
}
}
}
}

Related

Selection in Sectioned List in SwiftUI

If I have a list with multiple sections of heterogenous type (say like a sidebar in a macOS app), how do I keep track of the $selection? Here's a simplified example :
struct A: Hashable {
let propA: Int
}
struct B: Hashable {
let propB: Int
}
struct ContentView: View {
let allAs = (1...10).map{A(propA: $0)}
let allBs = (1...5).map{B(propB: $0)}
#Binding var selectedA: A?
#Binding var selectedB: B?
var body: some View {
List() { // What goes in selected: ?
Section() {
ForEach(allAs, id: \.propA) {
Text(String($0.propA))
}
} header: {
Text("A Section")
}
Section() {
ForEach(allBs, id: \.propB) {
Text(String($0.propB))
}
} header: {
Text("B Section")
}
}
}
}
Is the only way around this to create a generic type or protocol to hold A and B?

Nesting of several NavigationLink in a NavigationStack causes the loss of animation and breaks the backtracking

SwiftUI 4.0 introduces a new NavigationStack view.
Let's consider this simple structure.
struct Item: Identifiable, Hashable {
static let sample = [Item(), Item(), Item()]
let id = UUID()
}
When a NavigationLink is nested in another one, the navigation loses its animation and the backtracking takes directly to the root. Did I miss something, or is this a bug?
struct ItemDetailView: View {
let item: Item
var body: some View {
Text(item.id.uuidString)
}
}
struct ItemListView: View {
var body: some View {
List(Item.sample) { item in
NavigationLink(item.id.uuidString, value: item)
}
}
}
struct ExploreView: View {
var body: some View {
List {
Section {
NavigationLink {
ItemListView()
} label: {
Text("Items")
}
}
}
.navigationTitle("Explore")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
ExploreView()
}
}
}
Thanks!
Found the solution thanks to #Asperi's comment.
First, create a Hashable enum containing the destinations.
enum Destination: Hashable {
case items
var view: some View {
switch self {
case .items:
return ItemListView()
}
}
var title: LocalizedStringKey {
switch self {
case .items:
return "Items"
}
}
}
Next, use the new NavigationLink initializer.
NavigationLink(Destination.items.title, value: Destination.items)
And finally, add a new .navigationDestination modifier to catch all Destination values.
.navigationDestination(for: Destination.self) { destination in
destination.view
}

swift picker not selecting item

I have a short array of items that I want to display in a segmented picker. I'm passing the selected item (0, by default). The picker displays, but no item is selected, and the picker is unresponsive to clicks (in the simulator). I have a very similar picker that uses percentage values, and it works correctly. I am guessing that the issue has to do with the closure that I'm passing to the ForEach loop, but I am unclear on what syntax I should be using, if that is in fact the issue.
The code is as follows:
#State private var originalUnit = 0
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection $originalUnit) {
ForEach(sourceUnits, id: \.self {
Text($0)
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any insights on this would be appreciated. Thanks in advance!
You have a type mismatch between your originalUnit (Int) and your sourceUnits (String). Your selection needs to match the type.
struct ContentView: View {
#State private var originalUnit = "meters" //<-- Here
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection: $originalUnit) {
ForEach(sourceUnits, id: \.self) {
Text($0)
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
If, for some reason, you really needed originalUnit to be an Int, you could use enumerated (normally not the most efficient method for large collections in a ForEach, but that'll be inconsequential for something this small) so that the id is the index and matches the type (Int) of originalUnit:
struct ContentView: View {
#State private var originalUnit = 0
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection: $originalUnit) {
ForEach(Array(sourceUnits.enumerated()), id: \.0) { //<-- .0 is the index (Int)
Text($0.1) //<-- .1 is the original item String
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
You can set tag for your picker's row. Tag is any hashable type.
Refer this code work with any type of object for selection
struct TestView: View {
#StateObject var viewModel = TestViewModel()
var body: some View {
VStack {
Text(viewModel.selectedItem.title)
Picker("Select item", selection: $viewModel.selectedItem) {
ForEach(viewModel.items) { makeRowForItem($0) }
}
}
}
#ViewBuilder
func makeRowForItem(_ item: Item) -> some View {
Text(item.title).tag(item)
}
}
struct Item: Identifiable, Hashable {
var id = UUID().uuidString
var title = "Untitled"
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
class TestViewModel: ObservableObject {
#Published var selectedItem: Item
#Published var items: [Item]
init() {
let list = (1..<10).map { Item(title: "Untitled \($0)") }
items = list
selectedItem = list.first!
}
}

Updating a #State property from within a SwiftUI View

I have an AsyncContentView that handles the loading of data when the view appears and handles the switching of a loading view and the content (Taken from here swiftbysundell):
struct AsyncContentView<P:Parsable, Source:Loader<P>, Content: View>: View {
#ObservedObject private var source: Source
private var content: (P.ReturnType) -> Content
init?(source: Source, reloadAfter reloadTime:UInt64 = 0, #ViewBuilder content: #escaping (P.ReturnType) -> Content) {
self.source = source
self.content = content
}
func loadInfo() {
Task {
await source.loadData()
}
}
var body: some View {
switch source.state {
case .idle:
return AnyView(Color.clear.onAppear(perform: loadInfo))
case .loading:
return AnyView(ProgressView("Loading..."))
case .loaded(let output):
return AnyView(content(output))
}
}
}
For completeness, here's the Parsable protocol:
protocol Parsable: ObservableObject {
associatedtype ReturnType
init()
var result: ReturnType { get }
}
And the LoadingState and Loader
enum LoadingState<Value> {
case idle
case loading
case loaded(Value)
}
#MainActor
class Loader<P:Parsable>: ObservableObject {
#Published public var state: LoadingState<P.ReturnType> = .idle
func loadData() async {
self.state = .loading
await Task.sleep(2_000_000_000)
self.state = .loaded(P().result)
}
}
Here is some dummy data I am using:
struct Interface: Hashable {
let name:String
}
struct Interfaces {
let interfaces: [Interface] = [
Interface(name: "test1"),
Interface(name: "test2"),
Interface(name: "test3")
]
var selectedInterface: Interface { interfaces.randomElement()! }
}
Now I put it all together like this which does it's job. It processes the async function which shows the loading view for 2 seconds, then produces the content view using the supplied data:
struct ContentView: View {
class SomeParsableData: Parsable {
typealias ReturnType = Interfaces
required init() { }
var result = Interfaces()
}
#StateObject var pageLoader: Loader<SomeParsableData> = Loader()
#State private var selectedInterface: Interface?
var body: some View {
AsyncContentView(source: pageLoader) { result in
Picker(selection: $selectedInterface, label: Text("Selected radio")) {
ForEach(result.interfaces, id: \.self) {
Text($0.name)
}
}
.pickerStyle(.segmented)
}
}
}
Now the problem I am having, is this data contains which segment should be selected. In my real app, this is a web request to fetch data that includes which segment is selected.
So how can I have this view update the selectedInterface #state property?
If I simply add the line
self.selectedInterface = result.selectedInterface
into my AsyncContentView I get this error
Type '()' cannot conform to 'View'
You can do it in onAppear of generated content, but I suppose it is better to do it not directly but via binding (which is like a reference to state's external storage), like
var body: some View {
let selected = self.$selectedInterface
AsyncContentView(source: pageLoader) { result in
Picker(selection: selected, label: Text("Selected radio")) {
ForEach(result.interfaces, id: \.self) {
Text($0.name).tag(Optional($0)) // << here !!
}
}
.pickerStyle(.segmented)
.onAppear {
selected.wrappedValue = result.selectedInterface // << here !!
}
}
}

Require a SwitftUI View in a protocol without boilerplate

[ Ed: Once I had worked this out, I edited the title of this question to better reflect what I actually needed. - it wasn't until I answered my own question that I clarified what I needed :-) ]
I am developing an App using SwiftUI on IOS in which I have 6 situations where I will have a List of items which I can select and in all cases the action will be to move to a screen showing that Item.
I am a keen "DRY" advocate so rather than write the List Code 6 times I want to abstract away the list and select code and for each of the 6 scenarios I want to just provide what is unique to that instance.
I want to use a protocol but want to keep boilerplate to a minimum.
My protocol and associated support is this:
import SwiftUI
/// -----------------------------------------------------------------
/// ListAndSelect
/// -----------------------------------------------------------------
protocol ListAndSelectItem: Identifiable {
var name: String { get set }
var value: Int { get set }
// For listView:
static var listTitle: String { get }
associatedtype ItemListView: View
func itemListView() -> ItemListView
// For detailView:
var detailTitle: String { get }
associatedtype DetailView: View
func detailView() -> DetailView
}
extension Array where Element: ListAndSelectItem {
func listAndSelect() -> some View {
return ListView(items: self, itemName: Element.listTitle)
}
}
struct ListView<Item: ListAndSelectItem>: View {
var items: [Item]
var itemName: String
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(
destination: DetailView(item: item, index: String(item.value))
) {
VStack(alignment: .leading){
item.itemListView()
.font(.system(size: 15)) // Feasible that we should remove this
}
}
}
.navigationBarTitle(Text(itemName).foregroundColor(Color.black))
}
}
}
struct DetailView<Item: ListAndSelectItem>: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var item: Item
var index: String
var body: some View {
NavigationView(){
item.detailView()
}
.navigationBarTitle(Text(item.name).foregroundColor(Color.black))
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Text("<").foregroundColor(Color.black)}))
}
}
which means I can then just write:
struct Person: ListAndSelectItem {
var id = UUID()
var name: String
var value: Int
typealias ItemListView = PersonListView
static var listTitle = "People"
func itemListView() -> PersonListView {
PersonListView(person: self)
}
typealias DetailView = PersonDetailView
let detailTitle = "Detail Title"
func detailView() -> DetailView {
PersonDetailView(person: self)
}
}
struct PersonListView: View {
var person: Person
var body: some View {
Text("List View for \(person.name)")
}
}
struct PersonDetailView: View {
var person: Person
var body: some View {
Text("Detail View for \(person.name)")
}
}
struct ContentView: View {
let persons: [Person] = [
Person(name: "Jane", value: 1),
Person(name: "John", value: 2),
Person(name: "Jemima", value: 3),
]
var body: some View {
persons.listAndSelect()
}
}
which isn't bad but I feel I ought to be able to go further.
Having to write:
typealias ItemListView = PersonListView
static var listTitle = "People"
func itemListView() -> PersonListView {
PersonListView(person: self)
}
with
struct PersonListView: View {
var person: Person
var body: some View {
Text("List View for \(person.name)")
}
}
still seems cumbersome to me.
In each of my 6 cases I'd be writing very similar code.
I feel like I ought to be able to just write:
static var listTitle = "People"
func itemListView() = {
Text("List View for \(name)")
}
}
because that's the unique bit.
But that certainly won't compile.
And then the same for the Detail.
I can't get my head around how to simplify further.
Any ideas welcome?
The key to this is, if you want to use a view in a protocol then:
1) In the protocol:
associatedtype SpecialView: View
var specialView: SpecialView { get }
2) In the struct using the protocol:
var specialView: some View { Text("Special View") }
So in the situation of the question:
By changing my protocol to:
protocol ListAndSelectItem: Identifiable {
var name: String { get set }
var value: Int { get set }
// For listView:
static var listTitle: String { get }
associatedtype ListView: View
var listView: ListView { get }
// For detailView:
var detailTitle: String { get }
associatedtype DetailView: View
var detailView: DetailView { get }
}
I can now define Person as:
struct Person: ListAndSelectItem {
var id = UUID()
var name: String
var value: Int
static var listTitle = "People"
var listView: some View { Text("List View for \(name)") }
var detailTitle = "Person"
var detailView: some View { Text("Detail View for \(name)") }
}
which is suitable DRY and free of boilerplate!