How to align ZStack View to elements in a view below it - ios

I have some vstack views, and a zstack view (its a dropdown menu).
I need the top of the zstack view to align itself perfectly with the text of VStack 2 Text like this
I think I need to utilize named coordinate spaces, but I have been unsuccessful in accomplishing this

You can use a custom alignmentGuide:
struct ContentView: View {
#State private var selection: Int = 1
var body: some View {
ZStack(alignment: .myAlignment) { // important
VStack(spacing: 0) {
Text("VStack 1 - press long")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(.blue)
Text("VStack 2")
.padding(.trailing)
// defining alignment point on "VStack 2" Text
.alignmentGuide(.myVerticalAlignment, computeValue: { d in
d[VerticalAlignment.top]
})
.alignmentGuide(.myHorizontalAlignment, computeValue: { d in
d[HorizontalAlignment.trailing]
})
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(.orange)
Text("VStack 3")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(.pink)
Text("VStack 4")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(.green)
}
// Overlay
VStack {
Text("Item 1")
Text("Item 2")
Text("Item 3")
Text("Item 4")
Text("Item 5")
}
.padding()
.background(.gray)
// alignment of overlay
.alignmentGuide(.myVerticalAlignment, computeValue: { d in
d[VerticalAlignment.top]
})
.alignmentGuide(.myHorizontalAlignment, computeValue: { d in
d[HorizontalAlignment.leading]
})
}
}
}
extension VerticalAlignment {
private enum MyVerticalAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.bottom]
}
}
static let myVerticalAlignment = VerticalAlignment(MyVerticalAlignment.self)
}
extension HorizontalAlignment {
private enum MyHorizontalAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let myHorizontalAlignment = HorizontalAlignment(MyHorizontalAlignment.self)
}
extension Alignment {
static let myAlignment = Alignment(horizontal: .myHorizontalAlignment, vertical: .myVerticalAlignment)
}
But a much easier way for a dropdown menu would be using either .contextMenu (appearing on long press)
Text("VStack 1 - press long")
// option 1
.contextMenu {
Button("Item 1") {}
Button("Item 2") {}
Button("Item 3") {}
}
or a Menu style picker:
HStack {
Text("VStack 3")
// option 2
Picker("Menu", selection: $selection) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.pickerStyle(.menu)
}

Related

Swift UI Scroll View: Set frame for drag indicator

I have following problem. I want to create a vertical ScrollView with many rows. At the bottom of the view I have an info bar which appears over the scroll view because I put all the items in a ZStack. Here is my code and what it produces:
struct ProblemView: View {
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0..<100, id:\.self) {i in
HStack {
Text("Text \(i)")
.foregroundColor(.red)
Spacer()
Image(systemName: "plus")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Divider()
}
}
}
VStack {
Spacer()
HStack {
Text("Some Info here")
Image(systemName: "info.circle")
.foregroundColor(.blue)
}
.padding()
.frame(maxWidth: .infinity)
.ignoresSafeArea()
.background(.ultraThinMaterial)
}
}
}
}
struct ProblemView_Previews: PreviewProvider {
static var previews: some View {
ProblemView()
}
}
As you can see the drag indicator is hidden behind the info frame. Also the last item can't be seen because it is also behind the other frame. What
I want is that the drag indicator stops at this info frame. Why am I using a ZStack and not just a VStack? I want that this opacity effect behind the info frame, you get when you scroll.
A edit on my preview post has been added and therefore I cannot edit it... I am just gonna post the answer as an other one then.
This is the code that fixes your problem:
import SwiftUI
struct ProblemView: View {
var body: some View {
ScrollView {
VStack {
ForEach(0..<100, id:\.self) {i in
HStack {
Text("Text \(i)")
.foregroundColor(.red)
Spacer()
Image(systemName: "plus")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Divider()
}
}
.frame(maxWidth: .infinity)
}
.safeAreaInset(edge: .bottom) { // 👈🏻
VStack {
HStack {
Text("Some Info here")
Image(systemName: "info.circle")
.foregroundColor(.blue)
}
.padding()
.frame(maxWidth: .infinity)
.ignoresSafeArea()
.background(.ultraThinMaterial)
}
}
}
}
struct ProblemView_Previews: PreviewProvider {
static var previews: some View {
ProblemView()
}
}
We cannot control offset of indicator, but we can make all needed views visible by injecting last empty view with the same height (calculated dynamically) as info panel.
Here is possible approach. Tested with Xcode 13.2 / iOS 15.2
struct ProblemView: View {
#State private var viewHeight = CGFloat.zero
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0..<100, id:\.self) {i in
HStack {
Text("Text \(i)")
.foregroundColor(.red)
Spacer()
Image(systemName: "plus")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
Divider()
}
Color.clear
.frame(minHeight: viewHeight) // << here !!
}
}
VStack {
Spacer()
HStack {
Text("Some Info here")
Image(systemName: "info.circle")
.foregroundColor(.blue)
}
.padding()
.frame(maxWidth: .infinity)
.ignoresSafeArea()
.background(.ultraThinMaterial)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
}
.onPreferenceChange(ViewHeightKey.self) {
self.viewHeight = $0
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = value + nextValue()
}
}

TabView embedded in NavigationView pushes content down

I embedded a TabView in a NavigationView and the Text in the view gets pushed down slightly. I've tried moving the views around however it just ends up breaking functionally. You can see the green text is not vertically aligned to the rest of the device, but instead aligned to the content under the navigation bar.
var body: some View {
NavigationView{
TabView {
Text("TEST 1").foregroundColor(color).font(Font.custom("Catamaran-ExtraBold", size: 48)).navigationTitle("TEST 1")
Text("TEST 2").foregroundColor(color).font(Font.custom("Catamaran-ExtraBold", size: 48)).navigationTitle("TEST 2")
}
.foregroundColor(.black).navigationBarTitleDisplayMode(.inline)
.font(Font.custom("Catamaran-ExtraBold", size: 20))
.navigationBarItems(
leading:
NavigationLink(destination: SettingsView(), label: {
Image(systemName: "gearshape")
})).foregroundColor(colorScheme == .dark ? .white : .black)
}
.navigationViewStyle(.stack)
.ignoresSafeArea()
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
It's a bit strange what you are trying to achieve. Centering it as if the NavigationView wasn't there means that the view is no longer centered in its container. It will look strange and not centered like this.
However, here is how you can achieve this. This example uses GeometryReaders to measure the height of the navigation bar, and take away half that so the text now appears centered to the safe area.
Before:
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
Text("TEST 1")
.font(.title.bold())
.foregroundColor(.green)
.navigationTitle("TEST 1")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.1))
Text("TEST 2")
.font(.title.bold())
.foregroundColor(.green)
.navigationTitle("TEST 2")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.1))
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
.navigationViewStyle(.stack)
}
}
After:
struct ContentView: View {
var body: some View {
GeometryReader { geoRoot in // <- HERE
NavigationView {
GeometryReader { geo in // <- HERE
let yOffset = (geoRoot.safeAreaInsets.top - geo.safeAreaInsets.top) / 2 // <- HERE
TabView {
Text("TEST 1")
.font(.title.bold())
.foregroundColor(.green)
.navigationTitle("TEST 1")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.1))
// <- Also replicate offset here, not done for demo
Text("TEST 2")
.font(.title.bold())
.foregroundColor(.green)
.navigationTitle("TEST 2")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.1))
.offset(y: yOffset) // <- HERE
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
.navigationViewStyle(.stack)
}
}
}
Result
Before: TEST 1 tab
After: TEST 2 tab

Custom collapsible view

I'm trying to create a custom collapsible view. The code works but in Collapsible<Content: View> the VStack has strange behavior: the elements overlap when the component is closed.
To note this, try disable clipped() as shown in the image.
Is it a bug or something so stupid that I am not noticing?
Thanks in advance
FIXED CODE:
struct Collapsible<Content: View>: View {
var label: String
var content: () -> Content
init(label: String, #ViewBuilder _ content: #escaping () -> Content) {
self.label = label
self.content = content
}
#State private var collapsed: Bool = true
var body: some View {
VStack(spacing: 0) {
Button(action: {
withAnimation(.easeInOut) {
self.collapsed.toggle()
}
}, label: {
HStack {
Text(label)
Spacer(minLength: 0)
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding()
.background(Color.white.opacity(0.1))
}
)
.buttonStyle(PlainButtonStyle())
self.content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none, alignment: .top) // <- added `alignment` here
.clipped() // Comment to see the overlap
.animation(.easeOut)
.transition(.slide)
}
}
}
struct CollapsibleDemoView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Collapsible(label: "Collapsible") {
Text("Content")
.padding()
.background(Color.red)
}
Spacer(minLength: 0)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
The .frame modifier has a parameter alignment which defaults to center which results in the behaviour you're seeing: the layout bounds are set to zero height but the content is rendered vertically centered beyond the bounds (if not clipped). You can fix this by adding the alignment:
.frame(maxHeight: 0, alignment: .top)

How to show views on top of the Navigation Bar from within a child-linked view

I'm trying to have a sliding side menu cover the screen as well as the navigation bar. I was following this tutorial to achieve it, and it works well when you have the NavigationView declared within the actual view so you can embed it outside your NavigationView as a sibling in a ZStack.
However, anytime I try to pull this view in from a NavigationLink, the views on the sliding menu are "visible" while also blocked by the navigation bar and its views. Any ideas on how to fix this?
Side Menu code:
struct SlidingSideMenu<Content: View>: View {
let width: CGFloat
let isOpen: Bool
let menuClose: () -> Void
let content: Content
init(width: CGFloat, isOpen: Bool, menuClose: #escaping () -> Void, #ViewBuilder content: () -> Content) {
self.width = width
self.isOpen = isOpen
self.menuClose = menuClose
self.content = content()
}
var body: some View {
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.3))
.opacity(self.isOpen ? 1.0 : 0.0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.menuClose()
}
.edgesIgnoringSafeArea(.all)
HStack {
MenuContent(menuClose: menuClose) {
self.content
}
.frame(width: self.width)
.background(Color.white)
.offset(x: self.isOpen ? 0 : -self.width)
.animation(.default)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
Working:
struct MainDetailsView: View {
#State var menuOpen: Bool = false
var body: some View {
ZStack {
NavigationView {
ScrollView {
Text("Page Content")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarTitle("Nav title")
.navigationBarItems(leading: Button(action: { self.openMenu() }) {
Image(systemName: "magnifyingglass.circle.fill")
.font(.system(size: 25))
.foregroundColor(Color.secondary)
}.frame(maxWidth: .infinity, alignment: .leading))
}
SlidingSideMenu(width: 270,
isOpen: self.menuOpen,
menuClose: self.openMenu){
VStack{
Text("Side Menu Content")
Spacer()
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.edgesIgnoringSafeArea(.all)
}
}
func openMenu() {
self.menuOpen.toggle()
}
}
struct MainDetailsView_Previews: PreviewProvider {
static var previews: some View {
MainDetailsView()
}
}
Working GIF
Not Working:
struct ParentNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: MainDetailsView()) {
Text("N A V I G A T E")
}
}
}
}
struct MainDetailsView: View {
#State var menuOpen: Bool = false
var body: some View {
ZStack {
// NavView removed
ScrollView {
Text("Page Content")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarTitle("Nav title")
.navigationBarItems(leading: Button(action: { self.openMenu() }) {
Image(systemName: "magnifyingglass.circle.fill")
.font(.system(size: 25))
.foregroundColor(Color.secondary)
}.frame(maxWidth: .infinity, alignment: .leading))
SlidingSideMenu(width: 270,
isOpen: self.menuOpen,
menuClose: self.openMenu){
VStack{
Text("Side Menu Content")
Spacer()
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.edgesIgnoringSafeArea(.all)
}
}
func openMenu() {
self.menuOpen.toggle()
}
}
struct MainDetailsView_Previews: PreviewProvider {
static var previews: some View {
ParentNavView()
}
}
Not working GIF

SwiftUI2, Picker .onChange firing twice need value before change and after

I'm having an issue similar to the following post .onReceive firing twice.
I have a picker that fires .onChange twice. I am using a model data Environment object for the picker.
Is there a way for me to get the before state such that I can compare if the new_haveCount value is truely changing? Or better yet, to prevent the double fire in the first place?
#EnvironmentObject var modelData: ModelData
specifics and specificsFirebase are both structures.
Picker code
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.new_haveCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.new_haveCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[1]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.new_haveCount) { _ in
saveSpecifics()
}
From the apple dev page, .onChange seems to have a before and ofter property.
struct PlayerView : View {
var episode: Episode
#State private var playState: PlayState = .paused
var body: some View {
VStack {
Text(episode.title)
Text(episode.showTitle)
PlayButton(playState: $playState)
}
.onChange(of: playState) { [playState] newState in
model.playStateDidChange(from: playState, to: newState)
}
}
}
Full View if it helps
import SwiftUI
import Firebase
struct SpecificsEntryView: View {
#EnvironmentObject var modelData: ModelData
let figure: Figure
var figureIndex: Int {
modelData.figureArray.firstIndex(where: { $0.id == figure.id })!
}
var body: some View {
HStack(spacing: 4) {
// new labels
VStack(alignment: .leading, spacing: 4) {
ForEach(kSpecificType_Labels, id: \.self) { label in
Text(label)
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
Divider()
}
}
// new values
VStack(alignment: .center, spacing: 4) {
Text(kNewText)
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.new_haveCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.new_haveCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[1]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.new_haveCount) { _ in
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.new_wantCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.new_wantCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[2]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.new_wantCount) { _ in
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.new_sellCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.new_sellCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[3]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.new_sellCount) { _ in
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.new_orderCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.new_orderCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[4]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.new_orderCount) { _ in
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
} // end new vstack
Divider() // vertical
// loose values
VStack(alignment: .center, spacing: 4) {
Text(kLooseText)
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_haveCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_haveCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[1]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_haveCount) { _ in
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_wantCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_wantCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[2]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_wantCount) { newVal in
print("\(modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_wantCount) to \(newVal)")
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
Picker(selection: $modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_sellCount,
label: Text(" \(modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_sellCount) ")) {
ForEach(0 ..< 20) {
Text("\(kSpecificType_Labels[3]) \($0) \(kNewText.lowercased())")
}
}
.onChange(of: modelData.figureArray[figureIndex].specifics.specificsFirebase.loose_sellCount) { _ in
saveSpecifics()
}
.frame(maxHeight: .infinity)
.padding(.bottom, 2)
.pickerStyle(MenuPickerStyle())
Divider()
TextField("Order from", text: $modelData.figureArray[figureIndex].specifics.specificsFirebase.new_orderText,
onCommit: {
saveSpecifics()
})
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.padding(.bottom, 2)
.background(Color(.systemGray5))
.cornerRadius(4)
Divider()
} // end loose vstack
} // end all hstack specifics
.fixedSize(horizontal: false, vertical: true)
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
} // end body
// save specifics on update
func saveSpecifics() {
// Inject Firebase authentication
let userID = Auth.auth().currentUser?.uid
modelData.figureArray[figureIndex].specifics.specificsFirebase.saveSpecifics(userID: userID!)
}
}
I ran into the same capture list syntax issue and fixed it by using an explicit alias for the captured value from the EnvironmentObject variable (Xcode gave a hint). Like so:
.onChange(model.someVariable) {[oldValue = model.someVariable] newValue in { ... }
But my code still causes .onChange to fire twice...
I was having a similar issue, but I was getting 4 duplicate .onChange handler calls on a DatePicker control.
I ended up changing my code to use the Binding extension from here and the duplicate calls went away https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange
I'd still like to know what was causing the duplicate calls if someone has any insight.

Resources