I'm trying to reusable View and I added it on ContentView
This is my Child View
struct VStackView: View {
#Binding var spacing: Double
#Binding var alignmentIndex: Int
#Binding var elementsCount: Int
private let alignments: [HorizontalAlignment] = [.leading, .center, .trailing]
var body: some View {
VStack(alignment: self.alignments[alignmentIndex], spacing: CGFloat(spacing)) {
ForEach(0..<elementsCount) {
Text("\($0)th View")
}
}
}
}
and This is SuperView
Superview has Controls like Stepper, Slider, Picker that adjust values of VStack (alignment, spacing etc)
and I want to show the result depending on that values. but Child View is not changed
struct LayoutView: View {
private let layout: StackLayout
#State private var spacing = 0.0
#State private var alignmentIndex = 0
#State private var alignment: HorizontalAlignment = .leading
#State private var elementsCount: Int = 0
private let alignmentsString = [".leading", ".center", ".trailing"]
private let minValue = 0.0
private let maxValue = 100.0
init(_ layout: StackLayout) {
self.layout = layout
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Controls")) {
VStack(alignment: .leading) {
Text("Spacing: \(Int(spacing))").font(.caption)
HStack {
Text("\(Int(minValue))")
Slider(value: $spacing, in: minValue...maxValue, step: 1)
Text("\(Int(maxValue))")
}
Divider()
Picker("alignment", selection: $alignmentIndex) {
ForEach(0..<self.alignmentsString.count) {
Text("\(self.alignmentsString[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
Divider()
Stepper(value: $elementsCount, in: 0...10) {
Text("Element Count: \(elementsCount)")
}
}
}
VStackView(spacing: $spacing, alignmentIndex: $alignmentIndex, elementsCount: $elementsCount)
}
.navigationBarTitle(Text(layout.rawValue), displayMode: .inline)
}
}
}
I also search google and they recommend #EnviornmentObject. if that is correct, when to use #Binding property wrapper.
Isn't it two way binding properties?
Simply speaking you can use #Binding, when you want to share data in two places.
#Observable or #environmetobject is to be used, when you want to share your data in multiple views.
Your ForEach Loop in the VStackView generates a problem, because Swiftui does not know how it can identify each of your items uniquely so it does not know how to update them, when values change.
Append your code like this:
ForEach(0..<elementsCount, id: \.self) {
Text("\($0)th View")
}
Related
I am trying to make child view which contains string array moves back and forth with animation as button on parent view is toggled.
However, child view just show up and disappeared without any animation at all.
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = [one, two, three, four]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I changed code(stored property to viewmodel) as below. and It works as expected.
class ViewModel: ObservableObject {
#Published var values = [one, two, three, four]
}
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
#EnvironmentObject private vm: ViewModel
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { vm.value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I thought that toggling state redraws view only when with stored property.
But, child view with viewmodel is still redrawn when toggle state changes.
Data itself not changed at all. Please kindly let me know why this is happening.
there are some minor typos in your first code. If I correct them the code runs, and the animation works:
struct ContentView: View {
#State private var isToggle: Bool = false
var body: some View {
ChildView(isToggle: isToggle)
.onTapGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = ["one", "two", "three", "four"]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values, id: \.self) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
}
Using HalfASheet (https://github.com/franklynw/HalfASheet).
I have a View called ProjectsView, and in the ZStack in ProjectsView I have ProjectSorting and SortingView(both injected with the EnvironmentObject). I want the Text(🟩) in ProjectSorting to be changed, and the HStack(🟦) in SortingView to have a checkmark, both depending on the value of the sorting variable in SortingValues. Users can change the value of the sorting by pressing the Button in SortingView.
For whatever reason, the Text(🟩) in ProjectSorting does not change at all. And the HStack(🟦) in SortingView only gets the checkmark when its ancestor stack has another Text(🟨) which includes the #State variable from the environment, which I find very weird.
What should I change? Is there any way I can make this work using #EnvironmentObject? I'm a newbie and couldn't really understand other wrappers so I'd like to make this work within #State, #Binding, #EnvirionmentObject.
Thanks in advance.
SortingValues.swift
import Combine
class SortingValues: ObservableObject {
#Published var sorting = "Top Rated"
}
ProjectsView.swift
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
}
}
}
ProjectSorting.swift
import SwiftUI
struct ProjectSorting: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HStack {
Text("Projects")
Spacer()
Button {
showSortingSheet.toggle()
} label: {
HStack(spacing: 3) {
Image("sortingArrows")
Text(sortingValue) // < 🟩 this is the Text I want to be changed
}
}
}
// Another HStack goes here
}
.onReceive(sortingValues.$sorting) { sorting in
print("This is ProjectSorting. sorting:", sorting) // < this does not print when I close the half sheet
sortingValue = sorting
}
}
}
SortingView.swift
import SwiftUI
import HalfASheet
struct SortingView: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HalfASheet(isPresented: $showSortingSheet) {
let sorting = ["Most Recent", "Most Reviewed", "Top Rated", "Lowest Price", "Highest Price"]
VStack(alignment: .leading) {
ForEach(sorting, id: \.self) { sorting in
VStack(alignment: .leading, spacing: 14) {
Button (action: {
sortingValues.sorting = sorting
}, label: {
HStack { // 🟦
Text(sorting)
Spacer()
if sorting == sortingValue { // < this is where I add the checkmark
Image(systemName: "checkmark")
}
}
.foregroundColor(.primary)
})
if sorting != "Highest Price" {
Divider()
}
}
}
}
}
.height(.fixed(325))
// Text("Inside VStack, outside HalfASheet") // adding this Text DOES NOT make the HStack have a checkmark
Text("Inside VStack, outside HalfASheet: \(sortingValue)") // 🟨 adding this Text DOES make the HStack have a checkmark
}
.onReceive(sortingValues.$sorting) { sorting in
// the two printing lines below print correctly every time I tap the Button
print("This is SortingView. sorting:", sorting)
print("sortingValues.sorting: \(sortingValues.sorting)")
sortingValue = sorting
}
}
}
Your SortingView and ProjectSorting both access an environment object of type SortingValues, but you're passing new, separate instances to each. So the change you make in one place isn't being reflected in the other, because each view is communicating with one of two completely different objects of the same type.
If you want them to interact with the same object instance, you need to declare it at a point that's above both in the object hierarchy and make sure that that single instance is passed into both. For example:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValue)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValues)
}
}
}
But you can go one step further. Because environment objects and values propagate down the view hierarchy automatically, you can replace two separate .environmentObject calls with one:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
}
.environmentObject(sortingValues)
}
}
There are probably better ways of dealing with reacting to changes in your observed model rather than duplicating variable values in a local state variable -- but ensuring that all your views are using the same shared environment object should get you on your way.
I'm working on Tabview with page style and I want to scroll tabview on button actions. Buttons are added inside NavigationMenu.
NavigationMenu view and NavigationModel(ViewModel) are separated from a parent.
Selection handling is done inside NavigationModel.
On tab page swipe I'm able to see the change in NavigationMenu which is fine.
But if I tap on buttons the tabview page is not swiping. Even I receive change event on method onReceive.
Code:
import SwiftUI
import Combine
final class NavigationModel: ObservableObject {
#Published var selectedItem = ""
#Published var items: [String] = [
"Button 1", "Button 2", "Button 3"
]
}
struct NavigationMenu: View {
#ObservedObject var viewModel: NavigationModel
var body: some View {
HStack {
ForEach(0..<3, id: \.self) { index in
let title = viewModel.items[index]
Button {
viewModel.selectedItem = title
} label: {
Text(title)
.font(.system(.body))
.padding()
.foregroundColor(
viewModel.selectedItem == title ? .white : .black
)
.background(viewModel.selectedItem == title ? .black : .yellow)
}
}
}
}
}
final class TabViewModel: ObservableObject {
var navModel = NavigationModel()
}
struct TabviewWithMenuView: View {
#ObservedObject var viewModel = TabViewModel()
var body: some View {
parentView
}
private var parentView: some View {
VStack(spacing: 0) {
Spacer()
NavigationMenu(viewModel: viewModel.navModel)
pageView
}
.onReceive(viewModel.navModel.$selectedItem) { output in
print("Button tapped:", output)
}
}
private var pageView: some View {
TabView(selection: $viewModel.navModel.selectedItem) {
ForEach(0..<3, id: \.self) { index in
let tag = viewModel.navModel.items[index]
item(tag: tag)
.tag(tag)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.transition(.slide)
}
private func item(tag: String) -> some View {
VStack {
Text("PAGE: " + tag)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
}
}
Image:
ObservableObject inside ObservableObject is not observed, we need to observe explicitly the instance which is changed.
A possible solution in this case is to separate PageView and inject navigation view model to it so it would be observed.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
NavigationMenu(viewModel: viewModel.navModel)
PageView(navModel: viewModel.navModel)
...
struct PageView: View {
#ObservedObject var navModel: NavigationModel
var body: some View {
pageView
}
// ....
}
Test module in project is here
I have a navigation requirement that looks something like this:
Each detail screen can navigation to the next and previous detail screen. At the same time, the "back" button should always go back to the main list (not the previous detail screen).
I'm struggling with how to accomplish this in SwiftUI?
Here is what I have so far:
struct ListView: View {
#State private var currentDetailShown: Int?
#State private var listItems: [Int] = Array(repeating: 0, count: 10)
func goToNext() {
if let idx = self.currentDetailShown {
self.currentDetailShown = min(self.listItems.count - 1, idx + 1)
}
}
func goToPrev() {
if let idx = self.currentDetailShown {
self.currentDetailShown = max(0, idx - 1)
}
}
var body: some View {
List {
ForEach(0..<listItems.count) { index in
NavigationLink(destination: DetailView(goToNext: self.goToNext, goToPrev: self.goToPrev),
tag: index,
selection: self.$currentDetailShown) {
ListItem(score: listItems[index])
}
.isDetailLink(false)
.onTapGesture {
self.currentDetailShown = index
}
}
}
}
}
What happens with this code is that from the first detail view, it'll move to the to the next detail view and then immediately jump back to the list view.
I feel like I'm overthinking this or missing something obvious...
Instead of navigating to each detail from your list, you can navigate to a detailView that can show each detail individually by using a published variable in an observable object. Here is an example
struct MainView: View{
#EnvironmentObject var viewModel: ViewModel
var body: some View{
NavigationView{
VStack{
ForEach(self.viewModel.details, id:\.self){ detail in
NavigationLink(destination: DetailView(detail: self.viewModel.details.firstIndex(of: detail)!).environmentObject(ViewModel())){
Text(detail)
}
}
}
}
}
}
class ViewModel: ObservableObject{
#Published var showingView = 0
#Published var details = ["detail1", "detail2", "detail3", "detail4", "detail5", "detail6"]
}
struct DetailView: View{
#EnvironmentObject var viewModel: ViewModel
#State var detail: Int
var body: some View{
VStack{
IndivisualDetailView(title: viewModel.details[detail])
Button(action: {
self.viewModel.showingView -= 1
}, label: {
Image(systemName: "chevron.left")
})
Button(action: {
self.viewModel.showingView += 1
print(self.viewModel.showingView)
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct IndivisualDetailView: View{
#State var title: String
var body: some View{
Text(title)
}
}
In DoctorHomePage I have a grouped list and above the list I want to add a text view, but the text view doesn't show only if I change the font to a bigger one, but it is too big and I want it smaller. Here is my code:
import SwiftUI
import Combine
struct DoctorHomePage: View {
#Binding var shouldPopToRootView : Bool
#State private var curent: Int? = nil
#State private var isActive: Bool = false
#State private var id = 0
let defaults = UserDefaults.standard
let networkRequest = Network()
#State var cancelable: AnyCancellable? = nil
#State var localPatients : [Patients] = []
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentView(), tag: 1, selection: $curent) {
EmptyView()
}
Text("Welcome, doctor!") // this is the text that I want to add
.font(.system(size: 30)).fontWeight(.ultraLight)
.padding(.top, 50)
// PATIENT LIST
List(localPatients) { patient in
VStack(alignment: .leading) {
Text(patient.name)
}
}.listStyle(GroupedListStyle())
.onAppear(perform: {
self.loadPatients()
connCode = self.defaults.integer(forKey: "doctorID")
self.id = connCode
})
}.edgesIgnoringSafeArea([.top, .bottom])
}.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
Here are some screen shots to help you understand the problem:
The first image is with no text view.
The second image is with the font size of 60.
The third image is with the font size of 30.
Seems like some strange / buggy behavior.
Setting the zIndex of you welcome text will fix your problem.
Text("Welcome, doctor!").zIndex(1)