In SwiftUI, how to implement something like `navigationBarItems(...)` - swiftui

I've had some problems with SwiftUI's navigation API, so I'm experimenting with implementing my own. Parts of this are relatively easy: I create a class NavModel that is basically a stack. Depending on what's on the top of that stack, I can display different views.
But I can't see how to implement something like SwiftUI's .navigationBarItems(...). That view modifier seems to use something like the Preferences API to pass its argument View up the hierarchy to the containing navigation system. Eg:
VStack {
...
}.navigationBarItems(trailing: Button("Edit") { startEdit() })
Anything that goes through onPreferenceChange(...) has to be Equatable, so if I want to pass an AnyView? for the navigation bar items, I need to somehow may it Equatable, and I don't see how to do that.
Here's some sample code that shows a basic push and pop navigation. I'm wondering: how could I make the navBarItems(...) work? (The UI is ugly, but that's not important now.)
struct ContentView: View {
#StateObject var navModel: NavModel = .shared
var body: some View {
NavView(model: navModel) { node in
switch node {
case .root: rootView
case .foo: fooView
}
}
}
var rootView: some View {
VStack {
Text("This is the root")
Button {
navModel.push(.foo)
} label: {
Text("Push a view")
}
}
}
var fooView: some View {
VStack {
Text("Foo")
Button {
navModel.pop()
} label: {
Text("Pop nav stack")
}
}.navBarItems(trailing: Text("Test"))
}
}
struct NavView<Content: View>: View {
#ObservedObject var model: NavModel
let makeViews: (NavNode) -> Content
init(model: NavModel, #ViewBuilder makeViews: #escaping (NavNode) -> Content) {
self.model = model
self.makeViews = makeViews
}
#State var navItems: AnyView? = nil
var body: some View {
VStack {
let node = model.stack.last!
navBar
Divider()
makeViews(node)
.frame(maxHeight: .infinity)
// This doesn't compile
.onPreferenceChange(NavBarItemsPrefKey.self) { v in
navItems = v
}
}
}
var navBar: some View {
HStack {
if model.stack.count > 1 {
Button {
model.pop()
} label: { Text("Back") }
}
Spacer()
if let navItems = self.navItems {
navItems
}
}
}
}
enum NavNode {
case root
case foo
}
class NavModel: ObservableObject {
static let shared = NavModel()
#Published var stack: [NavNode]
init() {
stack = [.root]
}
func push(_ node: NavNode) { stack.append(node) }
func pop() {
if stack.count > 1 {
stack.removeLast()
}
}
}
struct NavBarItemsPrefKey: PreferenceKey {
typealias Value = AnyView?
static var defaultValue: Value = nil
static func reduce(value: inout Value, nextValue: () -> Value) {
let n = nextValue()
if n != nil { // ???
value = n
}
}
}
// Is this the right way? But then anything passed to navBarItems(...) would need
// to be Equatable. The common case - Buttons - are not.
struct AnyEquatableView: Equatable {
???
init<T>(_ ev: EquatableView<T>) {
???
}
static func == (lhs: AnyEquatableView, rhs: AnyEquatableView) -> Bool {
???
}
}
struct NavBarItemsModifier<T>: ViewModifier where T: View {
let trailing: T
func body(content: Content) -> some View {
content.preference(key: NavBarItemsPrefKey.self, value: AnyView(trailing))
}
}
extension View {
func navBarItems<T>(trailing: T) -> some View where T: View {
return self.modifier(NavBarItemsModifier(trailing: trailing))
}
}

Related

General view with a list of views that conform a protocol with function

I am trying to create a GeneralView in SwiftUI and would like to make the view take in a list of items where each item must conform to protocol HasVoidFunc and be a View. This is the closest I have been, and it works, but the problem is I have to manually add a new if statement it at some point I need TestButton3.
protocol HasVoidFunc {
buttonClick: () -> ()
}
struct GeneralView: View {
let list: [HasVoidFunc]
var body: some View {
HStack {
ForEach(0..<self.list.count) {
i in
if let v = self.list[i] as? TestButton1 {
v
}
else if let v = self.list[i] as? TestButton2 {
v
}
}
}
}
}
----------
struct TestButton1: View & HasVoidFunc {
var buttonClick: () -> ()
var body: some View {
Button(action: {
self.buttonClick()
}) {
Text("button1")
}
}
}
struct TestButton2: View & HasVoidFunc {
var buttonClick: () -> ()
var body: some View {
Button(action: {
self.buttonClick()
}) {
Text("button2")
}
}
}
struct GeneralView<T>: View where T: A, T: View {
var list: [T]
var body: some View {
VStack {
ForEach(list.indices, id: \.self) { i in
list[i]
}
}
}
}
No need to use AnyView and it's not recommended. list.indices for demo purposes only.

Show a placeholder when a SwiftUI List is empty [duplicate]

I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article

How to add a custom tap handler on a custom SwiftUI View?

List {
ItemView(item: item)
.myCustomTapHandler {
print("ItemView was tapped, triggered from List!")
}
}
}
struct ItemView: View {
var body: some View {
VStack {
Button(action: {
// this should fire myCustomTapHandler
}) {
Text("Hello world")
}
}
}
I have a custom ItemView with a simple button. I want to re create the same trailing closure syntax as .onTapGesture, with only triggering when you tap the Button. This will be named .myCustomTapHandler How to do this in SwiftUI?
What you are describing (The dot) is a ViewModifier
struct MyItemListView: View {
var body: some View {
List {
//Using a ViewModifier you make the whole View tappable not just the button
MyItemView(item: 2)
.myCustomTapHandler{
print("ItemView has custom modifier that makes the whole ItemView tappable")
}
}
}
}
struct MyCustomTapHandler: ViewModifier {
var myCustomTapHandler: () -> Void
func body(content: Content) -> some View {
content
//Add the onTap to the whole View
.onTapGesture {
myCustomTapHandler()
}
}
}
extension View {
func myCustomTapHandler(myCustomTapHandler: #escaping () -> Void) -> some View {
modifier(MyCustomTapHandler(myCustomTapHandler: myCustomTapHandler))
}
}
The ViewModifier affects the entire View not just the Button.
But, unless you are doing something else it is just an onTapGesture.
This is likely not the best solution because with the Button in the ItemView you will have inconsistent results.
Sometimes the Button will get the tap and sometimes the ViewModifier will get the tap and given that the View is in a List it will likely make the whole tapping confusing because the List has properties that make the whole row tappable anyway vs just the Text of the `Button
If you want the Button to perform an action that is defined in the ListView you can pass it as a parameter.
This will likely give you the best results.
struct MyItemListView: View {
var body: some View {
VStack {
//This passes the custom action to the button
MyItemView(item: 1){
print("Button needs to be tapped to trigger this")
}
}
}
}
struct MyItemView: View {
let item: Int
var myCustomTapHandler: () -> Void
var body: some View {
VStack {
Button(action: {
myCustomTapHandler()
}) {
Text("Hello world")
}
}
}
}
You can add like this.
struct ItemView: View {
private var action: (() -> Void)? = nil
var body: some View {
VStack {
Button(action: {
action?()
}) {
Text("Hello world")
}
}
}
func myCustomTapHandler(onAction: #escaping () -> Void) -> Self {
var view = self
view.action = onAction
return view
}
}
Usage
struct ContentView: View {
var body: some View {
ItemView()
.myCustomTapHandler {
print("Hello word")
}
}
}
Another way is...
I don't think with dot property you will get action. You need closure inside the ItemView.
Like this
struct ItemView: View {
var action: () -> Void
var body: some View {
VStack {
Button(action: action) {
Text("Hello world")
}
}
}
}
Usage
List {
ItemView {
// Here you will get action
print("ItemView was tapped, triggered from List!")
}
}

How to loop over viewbuilder content subviews in SwiftUI

So I’m trying to create a view that takes viewBuilder content, loops over the views of the content and add dividers between each view and the other
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// here
}
.background(Color.black)
.cornerRadius(14)
}
}
so where I wrote “here” I want to loop over the views of the content, if that makes sense. I’ll write a code that doesn’t work but that explains what I’m trying to achieve:
ForEach(content.subviews) { view in
view
Divider()
}
How to do that?
I just answered on another similar question, link here. Any improvements to this will be made for the linked answer, so check there first.
GitHub link of this (but more advanced) in a Swift Package here
However, here is the answer with the same TupleView extension, but different view code.
Usage:
struct ContentView: View {
var body: some View {
BoxWithDividerView {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle") // Different view types work!
}
}
}
Your BoxWithDividerView:
struct BoxWithDividerView: View {
let content: [AnyView]
init<Views>(#ViewBuilder content: #escaping () -> TupleView<Views>) {
self.content = content().getViews
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(content.indices, id: \.self) { index in
if index != 0 {
Divider()
}
content[index]
}
}
// .background(Color.black)
.cornerRadius(14)
}
}
And finally the main thing, the TupleView extension:
extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}
private struct GenericView {
let body: Any
var anyView: AnyView? {
AnyView(_fromValue: body)
}
}
private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}
let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}
Result:
So I ended up doing this
#_functionBuilder
struct UIViewFunctionBuilder {
static func buildBlock<V: View>(_ view: V) -> some View {
return view
}
static func buildBlock<A: View, B: View>(
_ viewA: A,
_ viewB: B
) -> some View {
return TupleView((viewA, Divider(), viewB))
}
}
Then I used my function builder like this
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#UIViewFunctionBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0.0) {
content()
}
.background(Color(UIColor.AdUp.carbonGrey))
.cornerRadius(14)
}
}
But the problem is this only works for up to 2 expression views. I’m gonna post a separate question for how to be able to pass it an array

Call parent's function from child of a child in SwiftUI

I have ParentView that passes a function to a ChildView, then the function is called in the ParentView when a button is clicked in the ChildView.
But what if I would want a Child of the Child to call the function? Would I need to pass the function further down, or is there a way to make a function somehow accessible throughout the environment?
struct ParentView: View {
func parentFunction() {
print("parentFunction called")
}
var body: some View {
ChildView(p: parentFunction)
}
}
struct ChildView: View {
var p: () -> ()
var body: some View {
VStack {
Text("child view")
Button(action: {
self.p()
}) {
Image(systemName: "person")
}
}
}
}
Yes it is possible to do using custom defined EnvironmentKey and then use it set parent view environment function, which will be available for all subviews.
Here is a demo of approach
struct ParentFunctionKey: EnvironmentKey {
static let defaultValue: (() -> Void)? = nil
}
extension EnvironmentValues {
var parentFunction: (() -> Void)? {
get { self[ParentFunctionKey.self] }
set { self[ParentFunctionKey.self] = newValue }
}
}
struct ParentView: View {
func parentFunction() {
print("parentFunction called")
}
var body: some View {
VStack {
ChildView()
}
.environment(\.parentFunction, parentFunction) // set in parent
}
}
struct ChildView: View {
#Environment(\.parentFunction) var parentFunction // join in child
var body: some View {
VStack {
Text("child view")
Button(action: {
self.parentFunction?() // < use in child
}) {
Image(systemName: "person")
}
Divider()
SubChildView()
}
}
}
struct SubChildView: View {
#Environment(\.parentFunction) var parentFunction // join in next subchild
var body: some View {
VStack {
Text("Subchild view")
Button(action: {
self.parentFunction?() // use in next subchild
}) {
Image(systemName: "person.2")
}
}
}
}