I am attempting to pass a Binding through my NavigationStack enum into my View. I'm not sure if I can pass Binding into an enum, if I cannot then how should I go about this. Thanks in advance!
#available(iOS 16.0, *)
enum Route: Hashable, Equatable {
//ERROR HERE: Not sure how to get Binding in enum or if possible
case gotoBView(input: Binding<String>)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
case .gotoBView(let input): BView1(bvar: input)
}
}
var isEmpty: Bool {
return false
}
}
//START VIEW
#available(iOS 16.0, *)
struct ContentView25: View {
#State var input = "Hello"
#State var path: NavigationPath = .init()
var body: some View {
NavigationStack(path: $path){
NavigationLink(value: Route.gotoBView(input: $input), label: {Text("Go To A")})
.navigationDestination(for: Route.self){ route in
route.view($path)
}
}
}
}
//View to navigate to with binding
#available(iOS 16.0, *)
struct BView1: View {
#Binding var bvar: String
var body: some View {
Text(bvar)
}
}
Here is a workaround, in previous iOS versions this has dismissed the NavigationLink, In iOS 16.2 it does not behave this way, I would do extensive testing before using this in a production app.
import SwiftUI
#available(iOS 16.0, *)
enum Route: Hashable, Equatable {
case gotoBView(input: Binding<String>)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
case .gotoBView(let input): BView1(bvar: input)
}
}
//Create a custom implementation of Hashable that ignores Binding
func hash(into hasher: inout Hasher) {
switch self {
case .gotoBView(let input):
hasher.combine(input.wrappedValue)
}
}
//Create a custom implementation of Equatable that ignores Binding
static func == (lhs: Route, rhs: Route) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
SwiftUI is all about identity and NavigationPath uses Hashable and Equatable to function. This bypasses SwiftUI's implementation.
With the stack, you don't need an enum, you can have multiple navigationDestination for each value type. To use it with a binding you can do it the old way, with a func that looks it up by ID, like how we did it before ForEach supported bindings, e.g.
struct NumberItem: Identifiable {
let id = UUID()
var number: Int
var text = ""
}
struct ContentView: View {
#State var numberItems = [NumberItem(number: 1), NumberItem(number: 2), NumberItem(number: 3), NumberItem(number: 4), NumberItem(number: 5)]
var body: some View {
NavigationStack {
List {
ForEach(numberItems) { numberItem in
NavigationLink(value: numberItem.id) {
Text("\(numberItem.number)")
}
}
}
.navigationDestination(for: NumberItem.ID.self) { numberItemID in
ChildDetailView(numberItems: $numberItems, numberItemID: numberItemID)
}
//.navigationDestination(for: AnotherItem.ID.self) { anotherItemID in
// ...
//}
}
}
}
// this wrapper View was originally needed to make bindings in
// navigationDestinations work at all, now its needed to fix a bug
// with the cursor jumping to the end of a text field which is
// using the binding.
struct ChildDetailView: View {
#Binding var numberItems: [NumberItem]
let numberItemID: UUID
var body: some View {
ChildDetailView2(numberItem: binding(for: numberItemID))
}
private func binding(for numberItemID: UUID) -> Binding<NumberItem> {
guard let index = numberItems.firstIndex(where: { $0.id == numberItemID }) else {
fatalError("Can't find item in array")
}
return $numberItems[index]
}
}
struct ChildDetailView2: View {
#Binding var numberItem: NumberItem
var body: some View {
VStack {
Text("\(numberItem.number)")
TextField("Test", text: $numberItem.text) // cursor jumps to the end if not wrapped in an extra View like this one is.
Button {
numberItem.number += 10
} label: {
Text("Add 10")
}
}
.navigationTitle("Detail")
}
}
Related
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))
}
}
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
I have this situation where I have a a BaseView containing some common elements and a BaseViewModel containing some common functions, but when its #Published var get updated no BaseView refresh occurs.
The setup is this:
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = EmptyView().convertToAnyView()
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
This view model subclasses BaseViewModel:
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(overlayView: OverlayView().convertToAnyView())
}
}
also I have a BaseView where I use the overlayView
struct BaseView<Content: View>: View {
let content: Content
#ObservedObject var viewModel = BaseViewModel()
init(content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content
viewModel.overlayView
}
}
}
The first view that gets displayed is FirstView, which conforms to a BaseViewProtocol and has a FirstViewModel that extends BaseViewModel.
struct FirstView: BaseViewProtocol {
#ObservedObject var viewModel = FirstViewModel()
var body: some View {
BaseView() {
Button("Show overlay") {
viewModel.showOverlayView()
}
}
}
}
Clicking the Show overlay button in First View calls the showOverlayView() func on FirstViewModel which in turn calls setOverlayView on the BaseViewModel. The value of overlayView in BaseViewModel changes as expected, but no view refresh on FirstView is called.
What am I doing wrong?
Thanks a lot.
I have just tested this code sample and works fine on Xcode 12 beta 6 & iOS 14 beta 8
struct ContentView: View {
#StateObject private var viewModel = FirstViewModel()
var body: some View {
ZStack {
Button(action: { viewModel.showOverlayView() }) {
Text("Press")
}
viewModel.overlayView
}
}
}
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = AnyView(EmptyView())
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(
overlayView: AnyView(
Color.blue
.opacity(0.2)
.allowsHitTesting(false)
)
)
}
}
Generally in SwiftUI you don't create views in outside the body. The view creation should be left to SwiftUI - instead you can define some other controls telling SwiftUI how and when to create a view.
Here is a simplified demo how to present different overlays for different views.
You can create a basic OverlayView:
enum OverlayType {
case overlay1, overlay2
}
struct OverlayView: View {
let overlayType: OverlayType
#ViewBuilder
var body: some View {
if overlayType == .overlay1 {
Text("Overlay1") // can be replaced with any view you want
}
if overlayType == .overlay2 {
Text("Overlay1")
}
}
}
and use it in your BaseView (if overlayType is nil the overlay will not be shown):
struct BaseView<Content>: View where Content: View {
let overlayType: OverlayType?
let content: () -> Content
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content()
if overlayType != nil {
OverlayView(overlayType: overlayType!)
}
}
}
}
Now in the ContentView you can use the BaseView and specify its OverlayType.
struct ContentView: View {
#State var overlayType: OverlayType?
var body: some View {
BaseView(overlayType: overlayType) {
Button("Show overlay") {
overlayType = .overlay1
}
}
}
}
Some considerations:
For simplicity I used #State variables to control overlays. If there are other use cases for your ViewModels you may want to move the logic there.
Note that instead of AnyView it's preferred to use #ViewBuilder.
Also if you want to observe an ObservableObject inside a view, you need to use #ObservedObject, not #ObservableObject.
Im trying to create an environment object that is editable and putting it in a list.
The Variables are only refreshing when I switch the tab for example (so whenever I leave the NavigationView) and then come back.
The same worked with a ModalView before. Is it a bug maybe? Or am I doing something wrong?
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
try passing .environmentObject on your destination:
NavigationLink(destination: detailView(feed: self._feed, i: i).environmentObject(x))
You have to use willSet instead of didSet.
TestItem should be a value type: struct or enum. SwiftUI's observation system properly works only with value types.
I try to run a function in a VStack statement but it don't work. When I run it in a button (with the action label) it work perfectly. How can I insert my func in a VStack?
I declare a QuizData class:
class QuizData: ObservableObject {
var allQuizQuestion: [QuizView] = [QuizView]()
let objectWillChange = PassthroughSubject<QuizData,Never>()
var currentQuestion: Int = 0 {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
and I use it there :
struct Quiz: View {
var continent: Continent
#EnvironmentObject var quizData: QuizData
var body: some View {
VStack
{
generateQuiz(continent: continent, quizData: self.quizData)
quizData.allQuizQuestion[quizData.currentQuestion]
}
.navigationBarTitle (Text(continent.name), displayMode: .inline)
}
}
The func generateQuiz is:
func generateQuiz(continent: Continent, quizData: QuizData) -> Void {
var capital: [Capital]
var alreadyUse: [Int]
for country in CountryData {
if country.continentId == continent.id
{
alreadyUse = [Int]()
capital = [Capital]()
capital.append(CapitalData[country.id])
for _ in 1...3 {
var index = Int.random(in: 1 ... CapitalData.count - 1)
while alreadyUse.contains(index) {
index = Int.random(in: 1 ... CapitalData.count - 1)
}
capital.append(CapitalData[index])
}
capital.shuffle()
quizData.allQuizQuestion.append(QuizView(country: country, question: QuestionData[country.id], capital: capital))
}
}
quizData.allQuizQuestion.shuffle()
}
I need to generate quiz question before the view appear. How should I do this?
First, you can't call a function that doesn't return some View in a VStack closure because that closure is not a normal closure, but a #ViewBuilder closure:
#functionBuilder
struct ViewBuilder {
// Build a value from an empty closure, resulting in an
// empty view in this case:
func buildBlock() -> EmptyView {
return EmptyView()
}
// Build a single view from a closure that contains a single
// view expression:
func buildBlock<V: View>(_ view: V) -> some View {
return view
}
// Build a combining TupleView from a closure that contains
// two view expressions:
func buildBlock<A: View, B: View>(_ viewA: A, viewB: B) -> some View {
return TupleView((viewA, viewB))
}
// And so on, and so forth.
...
}
It's a Swift 5.1 feature that lets you do things like these:
VStack {
Image(uiImage: image)
Text(title)
Text(subtitle)
}
With which you can easily create a view from several other views. For further information take a look at https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api
Now, if I get your issue (correct me if I'm wrong) you need to call a function before your view appears to generate some data. Honestly I'd prefer to pass that data to the view from the outside (creating the data before the view creation). But if you really need it you can do something like:
struct ContentView: View {
private var values: [Int]! = nil
init() {
values = foo()
}
var body: some View {
List(values, id: \.self) { val in
Text("\(val)")
}
}
func foo() -> [Int] {
[0, 1, 2]
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Using the struct init and calling the function at the view creation.
EDIT: To answer your comment here below and since you are using an #EnvironmentObject you can do:
class ContentViewModel: ObservableObject {
#Published var values: [Int]!
init() {
values = generateValues()
}
private func generateValues() -> [Int] {
[0, 1, 2]
}
}
struct ContentView: View {
#EnvironmentObject var contentViewModel: ContentViewModel
var body: some View {
List(contentViewModel.values, id: \.self) { val in
Text("\(val)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ContentViewModel()) //don't forget this
}
}
#endif
And in your SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(ContentViewModel()) //don't forget this
)
self.window = window
window.makeKeyAndVisible()
}
}
This way you are creating a view model for your view and that view model will be accessible throughout your view hierarchy. Every time your view model will change your view will change too.