SwiftUI: Spacing between List Items - swiftui

I have some code that displays a list with a title and a rectangle and if the user taps on it, it navigates to a detail page.
But my code seems to have multiple problems:
The spacer is not working. I want the name on the left and the rectangle on the right like a spacer in a Stack would push it.
The background color of the rectangle is not applied. It is just black
If I add a navigationTitle, I see constraint errors (see below)
(
"<NSLayoutConstraint:0x600002438f50 'BIB_Trailing_CB_Leading' H:[_UIModernBarButton:0x7fd15fe088b0]-(6)-[_UIModernBarButton:0x7fd15fe064d0'World Clock'] (active)>",
"<NSLayoutConstraint:0x600002438fa0 'CB_Trailing_Trailing' _UIModernBarButton:0x7fd15fe064d0'World Clock'.trailing <= _UIButtonBarButton:0x7fd15fe05310.trailing (active)>",
"<NSLayoutConstraint:0x600002439d10 'UINav_static_button_horiz_position' _UIModernBarButton:0x7fd15fe088b0.leading == UILayoutGuide:0x600003e04a80'UIViewLayoutMarginsGuide'.leading (active)>",
"<NSLayoutConstraint:0x600002439d60 'UINavItemContentGuide-leading' H:[_UIButtonBarButton:0x7fd15fe05310]-(0)-[UILayoutGuide:0x600003e049a0'UINavigationBarItemContentLayoutGuide'] (active)>",
"<NSLayoutConstraint:0x6000024350e0 'UINavItemContentGuide-trailing' UILayoutGuide:0x600003e049a0'UINavigationBarItemContentLayoutGuide'.trailing == _UINavigationBarContentView:0x7fd15fd0a640.trailing (active)>",
"<NSLayoutConstraint:0x60000243a4e0 'UIView-Encapsulated-Layout-Width' _UINavigationBarContentView:0x7fd15fd0a640.width == 0 (active)>",
"<NSLayoutConstraint:0x6000024354a0 'UIView-leftMargin-guide-constraint' H:|-(0)-UILayoutGuide:0x600003e04a80'UIViewLayoutMarginsGuide' (active, names: '|':_UINavigationBarContentView:0x7fd15fd0a640 )>"
)
Here is my code:
var body: some View {
NavigationView {
List {
ForEach(viewModel.cityList, id: \.name) { city in
NavigationLink(
destination: CityDetailView(city: city)){
HStack{
Text(city.name)
Spacer()
CustomView()
}
}.frame(height: 100)
}
}
.navigationTitle("World Clock")
}
}
CustomView:
var body: some View {
GeometryReader { geo in
Circle().frame(width: geo.size.height, height: geo.size.height)
.background(Color.green)
}
}
Any idea what is wrong?

Your spacer doesn't work because there's NavigationLink in the HStack which took all the space: it has bigger priority than Spacer.
If you wanna use NavigationLink with EmptyView, it should be placed in a ZStack with nearby view, but in this case you can simply place content of your cell inside NavigationLink label.
Rectangle is not a view but a Shape, and as with any other Shape its color should be set with foregroundColor, not with background.
GeometryReader always takes all available space, unless you're adding size modifiers to it. It cannot wrap around content by itself. So you can use following trick: add onAppear for an item inside GeometryReader and pass width to state variable, which will then add width modifier to GeometryReader.
struct ContentView: View {
#ObservedObject var viewModel = CityListViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.cityList, id: \.name) { city in
NavigationLink(
destination: Text(city.name)){
HStack{
Text(city.name)
Spacer()
CustomView()
}
}.frame(height: 100)
}
}
.navigationTitle("World Clock")
}
}
}
struct CustomView: View {
#State
var width: CGFloat?
var body: some View {
GeometryReader { geo in
Circle().frame(width: geo.size.height, height: geo.size.height)
.background(Color.green)
.onAppear {
width = geo.size.height
}
}.frame(width: width)
}
}
If you see some NSLayoutConstraint broken which you didn't create, like when using SwiftUI, you can ignore it: it's not your bug.

Related

SwiftUI extend or recalculate VStack height when children views extend

I have a scrollview that holds "cards" with weather details of locations and the cards extend to show more information when tapped.
I have to use a LegacyScrollView so that the bottomsheet that encompasses the scrollview gets dragged down when the the scroll is at the top.
I can't figure out how to extend the VStack when one of the cards extend. Is there a way to make a GeometryReader or VStack recalculate the needed height to hold all of the views?
I start off with the minHeight set to the window size so when the 1st card is extended, it all shows, but 2 or more extend past the LazyVStack window and get cut off.
If the minHeight on the LaxyVStack isn't set to 0, the cells expand from the middle and their tops get cut off. When it's set to 0, the cells expand downward as desired.
GeometryReader { proxy in
LegacyScrollView(.vertical, showsIndicators: false) {
Spacer(minLength: 40)
LazyVStack(spacing: 0) {
ForEach(mapsViewModel.placedMarkers, id: \.id) { _placeObject1 in
InfoCell(placeObject: _placeObject1)
.environmentObject(mapsViewModel)
.frame(alignment: .top)
Divider().foregroundColor(.white)
}
}
.cornerRadius(25)
.frame(minHeight: 0, alignment: .top)
}
.onGestureShouldBegin{ pan, scrollView in
scrollView.contentOffset.y > 0 || pan.translation(in: scrollView).y < 0
}
.onScroll{ test in
print(test.contentOffset.y)
}
.padding(.top, 30) //padding for top of list
}
and the cell's code is :
struct InfoCell: View {
#StateObject var placeObject: PlaceInfo
#State var expandCell = false
var body: some View {
VStack(alignment: .center, spacing: 0) {
//blahblah
if expandCell {
CellExpanded(placeObject: placeObject, isFirstInList: true)
}
}
.frame(maxWidth: .infinity)
.onTapGesture {
withAnimation { expandCell.toggle() }
}
}
}
and the expand code:
struct InfoCellExpanded: View {
#State var forecast = "Daily"
var placeObject: PlaceInfo
var isFirstInList: Bool
var body: some View {
VStack {
//blah
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
}

SwiftUI animation not working using animation(_:value:)

In SwiftUI, I've managed to make a Button animate right when the view is first drawn to the screen, using the animation(_:) modifier, that was deprecated in macOS 12.
I've tried to replace this with the new animation(_:value:) modifier, but this time nothing happens:
So this is not working:
struct ContentView: View {
#State var isOn = false
var body: some View {
Button("Press me") {
isOn.toggle()
}
.animation(.easeIn, value: isOn)
.frame(width: 300, height: 400)
}
}
But then this is working. Why?
struct ContentView: View {
var body: some View {
Button("Press me") {
}
.animation(.easeIn)
.frame(width: 300, height: 400)
}
}
The second example animates the button just as the view displays, while the first one does nothing
The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.
animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.
struct ContentView: View {
#State var isOn = false
//The better route is to have a separate variable to control the animations
// This prevents unpleasant side-effects.
#State private var animate = false
var body: some View {
VStack {
Text("I don't change.")
.padding()
Button("Press me, I do change") {
isOn.toggle()
animate = false
// Because .opacity is animated, we need to switch it
// back so the button shows.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
animate = true
}
}
// In this case I chose to animate .opacity
.opacity(animate ? 1 : 0)
.animation(.easeIn, value: animate)
.frame(width: 300, height: 400)
// If you want the button to animate when the view appears, you need to change the value
.onAppear { animate = true }
}
}
}
Follow up question: animating based on a property of an object is working on the view itself, but when I'm passing that view its data through a ForEach in the parent view, an animation modifier on that object in the parent view is not working. It won't even compile. The objects happen to be NSManagedObjects but I'm wondering if that's not the issue, it's that the modifier works directly on the child view but not on the passed version in the parent view. Any insight would be greatly appreciated
// child view
struct TileView: View {
#ObservedObject var tile: Tile
var body: some View {
Rectangle()
.fill(tile.fillColor)
.cornerRadius(7)
.overlay(
Text(tile.word)
.bold()
.font(.title3)
.foregroundColor(tile.fillColor == .myWhite ? .darkBlue : .myWhite)
)
// .animation(.easeInOut(duration: 0.75), value: tile.arrayPos)
// this modifier worked here
}
}
struct GridView: View {
#ObservedObject var game: Game
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)
var body: some View {
GeometryReader { geo in
LazyVGrid(columns: columns) {
ForEach(game.tilesArray, id: \.self) { tile in
Button(action: {
tile.toggleSelectedStatus()
moveTiles() <- this changes their array position (arrayPos), and
the change in position should be animated
}) {
TileView(tile: tile)
.frame(height: geo.size.height * 0.23)
}
.disabled(tile.status == .solved || tile.status == .locked)
.animation(.easeInOut(duration: 0.75), value: arrayPos)
.zIndex(tile.status == .locked ? 1 : 0)
}
}
}
}
}

SwiftUI add border to onSelect horizontal Scrollview image

I tried to make a horizontal scrollview that contained a list of item. when I clicked any of the item, it will show a border(highlight) behind the item. When I press another item, the previous border disappear and the latest clicked item appear the border. It just looks like a selection horizontal scrollview. I have no idea about how to do so.
ScrollView(.horizontal, showsIndicators: false){
LazyHStack{
ForEach(self.staffs.indices, id: \.self){ staff in
VStack{
Image(staff.image)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
.onTapGesture {
print(createBookingJSON.staffs[staff].staffID!)
}
}
}
}
Convert your VStack into a standalone view, then pass a Binding to it that can be read from the Parent View. Your new VStack standalone view needs an OnTapGesture or an Action through a button to toggle it's state. We will make your ForEach a "single selection" list as you request.
NEW View to use inside your ForEach:
struct ItemCell: View {
var item: Item
#Binding var selectedItem: Item?
var body: some View {
VStack{
Image(item.image)
.resizable()
.frame(width: 100, height: 100)
.border(Color.green, width: (item == selectedItem) ? 20 : 0)
}
.onTapGesture {
self.selectedItem = item
print(createBookingJSON.staffs[staff].staffID!)
}
}
}
Now in the view that contains your Foreach, add a State Var of selectedItem so you can read the Binding you created in your cells. And Replace your VStack with your new ItemCell:
struct YourParentView: View {
#State var selectedItem: Item? = nil
var body: some View {
ScrollView(.horizontal, showsIndicators: false){
LazyHStack{
ForEach(self.staffs.indices, id: \.self){ staff in
ItemCell(item: staff, selectedItem: self.$selectedItem)
}
}
}
}
}
Now when you click on the item, a border should appear. You may need to play with the border depending on your design, and the clipShape you have used of Circle(). Good Luck comrade.

Size a SwiftUI view to be safeAreaInsets.top + 'x'

I am trying to create a full bleed SwiftUI 'header' view in my scene. This header will sit within a List or a scrollable VStack.
In order to do this, I'd like to have my text in the header positioned below the safe area, but the full view should extend from the top of the screen (and thus, overlap the safe area). Here is visual representation:
V:[(safe-area-spacing)-(padding)-(text)]
here is my attempt:
struct HeaderView: View {
#State var spacing: CGFloat = 100
var body: some View {
HStack {
VStack(alignment: .leading) {
Rectangle()
.frame(height: spacing)
.opacity(0.5)
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer()
}
.frame(maxWidth: .infinity)
.background(Color.red)
.background(
GeometryReader { proxy in
Color.clear
.preference(
key: SafeAreaSpacingKey.self,
value: proxy.safeAreaInsets.top
)
}
)
.onPreferenceChange(SafeAreaSpacingKey.self) { value in
self.spacing = value
}
}
}
This however, does not seem to correctly size 'Rectangle'. How can I size a view according to the safe area?
Is this what you're looking for? I try to avoid using GeometryReader unless you really need it... I created a MainView, which has a background and a foreground layer. The background layer will ignore the safe areas (full bleed) but the foreground will stay within the safe area by default.
struct HeaderView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer(minLength: 0)
}
}
}
struct MainView: View {
var body: some View {
ZStack {
// Background
ZStack {
}
.frame(maxWidth:. infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
// Foreground
VStack {
HeaderView()
Spacer()
}
}
}
}
add an state to store desired height
#State desiredHeight : CGFloat = 0
then on views body :
.onAppear(perform: {
if let window = UIApplication.shared.windows.first{
let phoneSafeAreaTopnInset = window.safeAreaInsets.top
desiredHeight = phoneSafeAreaTopnInset + x
}
})
set the desiredHeight for your view .
.frame(height : desiredHeight)

In SwiftUI how can I animate a button offset when displayed

In SwiftUI, I want a button to appear from off screen by dropping in from the top into a final position when the view is initially displayed, I'm not asking for animation when the button is pressed.
I have tried:
Button(action: {}) {
Text("Button")
}.offset(x: 0.0, y: 100.0).animation(.basic(duration: 5))
but no joy.
If you would like to play with offset, this can get you started.
struct ContentView : View {
#State private var offset: Length = 0
var body: some View {
Button(action: {}) { Text("Button") }
.offset(x: 0.0, y: offset)
.onAppear {
withAnimation(.basic(duration: 5)) { self.offset = 100.0 }
}
}
}
I first suggested a .transition(.move(.top)), but I am updating my answer. Unless your button is on the border of the screen, it may not be a good fit. The move is limited to the size of the moved view. So you may need to use offset after all!
Note that to make it start way out of the screen, the initial value of offset can be negative.
First of all you need to create a transition. You could create an extension for AnyTransition or just create a variable. Use the move() modifier to tell the transition to move the view in from a specific edge
let transition = AnyTransition.move(edge: .top);
This alone only works if the view is at the edge of the screen. If your view is more towards the center you can use the combined() modifier to combine another transition such as offset() to add additional offset
let transition = AnyTransition
.move(edge: .top)
.combined(with:
.offset(
.init(width: 0, height: 100)
)
);
This transition will be for both showing and removing a view although you can use AnyTransition.asymmetric() to use different transitions for showing and removing a view
Next create a showButton bool (name this whatever) which will handle showing the button. This will use the #State property wrapper so SwiftUI will refresh the UI when changed.
#State var showButton: Bool = false;
Next you need to add the transition to your button and wrap your button within an if statement checking if the showButton bool is true
if (self.showButton == true) {
Button(action: { }) {
Text("Button")
}
.transition(transition);
}
Finally you can update the showButton bool to true or false within an animation block to animate the button transition. toggle() just reverses the state of the bool
withAnimation {
self.showButton.toggle();
}
You can put your code in onAppear() and set the bool to true so the button is shown when the view appears. You can call onAppear() on most things like a VStack
.onAppear {
withAnimation {
self.showButton = true;
}
}
Check the Apple docs to see what is available for AnyTransition https://developer.apple.com/documentation/swiftui/anytransition
Presents a message box on top with animation:
import SwiftUI
struct MessageView: View {
#State private var offset: CGFloat = -200.0
var body: some View {
VStack {
HStack(alignment: .center) {
Spacer()
Text("Some message")
.foregroundColor(Color.white)
.font(Font.system(.headline).bold())
Spacer()
}.frame(height: 100)
.background(Color.gray.opacity(0.3))
.offset(x: 0.0, y: self.offset)
.onAppear {
withAnimation(.easeOut(duration: 1.5)) { self.offset = 000.0
}
}
Spacer()
}
}
}
For those that do want to start from a Button that moves when you tap on it, try this:
import SwiftUI
struct ContentView : View {
#State private var xLoc: CGFloat = 0
var body: some View {
Button("Tap me") {
withAnimation(.linear(duration: 2)) { self.xLoc+=50.0 }
}.offset(x: xLoc, y: 0.0)
}
}
Or alternatively (can replace Text with anything):
Button(action: {
withAnimation(.linear(duration: 2)) { self.xLoc+=50.0 }
} )
{ Text("Tap me") }.offset(x: xLoc, y: 0.0)