How to layout properly in ZStack (I have visibility problem)? - swiftui

Here is reproducable small code below;
As you'll see when you run the demo code, the Element view does stay under Color.blue when dragged eventhough its above according to ZStack. By the way I also played with zIndex modifier but still no luck. Any solution you offer? Thanks all.
struct ContentView: View {
var body: some View {
GeometryReader { gr in
ZStack {
Color.blue.opacity(0.3)
.aspectRatio(1, contentMode: .fit)
.frame(width: gr.size.width)
VStack {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(1...15, id: \.self) { (idx) in
Element(index: idx)
}
}
.padding()
}
.background(Color.secondary.opacity(0.3))
}
}
}
}
}
struct Element: View {
#State private var dragAmount = CGSize.zero
var index: Int
var body: some View {
Rectangle()
.frame(width: 80, height: 80)
.overlay(Text("\(index)").bold().foregroundColor(.white))
.offset(dragAmount)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmount = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded { _ in
self.dragAmount = .zero
}
)
}
}

iOS 15.5: still valid
How can achieve my goal then, like dragging Element on different view (in this scenario Color.blue)
Actually we need to disable clipping by ScrollView.
Below is possible approach based on helper extensions from my other answers (https://stackoverflow.com/a/63322713/12299030 and https://stackoverflow.com/a/60855853/12299030)
VStack {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(1...15, id: \.self) { (idx) in
Element(index: idx)
}
}
.padding()
.background(ScrollViewConfigurator {
$0?.clipsToBounds = false // << here !!
})
}
.background(Color.secondary.opacity(0.3))
}

Related

How to get rid of NavigationLink space

I have a problem with space occupied by NavigationLink. Following code:
struct EditView: View {
var body: some View {
NavigationView {
Form {
Section("Colors") {
ColorList(colors: viewModel.game.gameColors)
}
}
}
}
}
struct ColorList: View {
let colors: [String]
private let gridItemLayout = [GridItem(.adaptive(minimum: 44))]
var body: some View {
ScrollView {
LazyVGrid(columns: gridItemLayout) {
ForEach(colors, id: \.self) { colorName in
Meeple(colorName: colorName)
}
.padding(.vertical, 2)
}
}
}
}
// Meeple is just an image
struct Meeple: View {
// ...
var body: some View {
Image("meeple.2.fill")
.resizable()
.padding(5)
.foregroundColor(color.color)
.background(color.backgroundColor)
.frame(width: 44, height: 44, alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 5))
.shadow(color: .primary, radius: 5)
}
}
Produces a good result:
As soon as I add a NavigationLink around the ColorList like so
Section("Colors") {
NavigationLink(destination:
MultiColorPickerView(
selection: $viewModel.game.colors.withDefaultValue([])
)
) {
ColorList(colors: viewModel.game.gameColors)
}
}
The result looks weird:
There's plenty of space left. Why does it break after 3 items? And how can I make it to show more in one line?
add .frame(maxWidth: .infinity) to your ColorList.

SwiftUI Popover Size is not expanding to fit content

Here is my code
struct ContentView: View {
#State var showingPopover = false
var body: some View {
VStack {
Spacer()
Text("Hello World")
Spacer()
HStack {
Spacer()
Button {
self.showingPopover.toggle()
} label: {
Image(systemName: "plus.circle")
}
.popover(isPresented: $showingPopover) {
List(0..<100) { Text("\($0)") }
}.padding(30)
}
}
}
}
This should produce a really nice popover coming from the plus button. But all I get is a really squashed down popover.
Any idea what I am missing here? Is there a way to tell the popover to expand more (without specifying a size)?
You may use a ScrollView and ForEach instead of a List:
struct ContentView: View {
#State var showingPopover = false
var body: some View {
VStack {
Spacer()
Text("Hello World")
Spacer()
HStack {
Spacer()
Button(action: {
self.showingPopover.toggle()
}) {
Image(systemName: "plus.circle")
}
.padding(30)
}
}
// can be attached to the button as well (as in the question)
.popover(isPresented: $showingPopover,
attachmentAnchor: .point(.bottomTrailing),
arrowEdge: .bottom) {
ScrollView(.vertical, showsIndicators: false) {
ForEach(0 ..< 100) {
Text("\($0)")
}
}
}
}
}
You can provide a custom frame for the List. Also, don't forget to embed List inside a ScrollView if you want it to scroll.
ScrollView {
List(0..<100) {
Text("\($0)")
}
.frame(width: 100, height: 250)
}

How to make SwiftUI to forget internal state

I have a view like following
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
// Common Elements
content
// More Common Elements
}
}
}
}
When I call this from another view like
A(nextInnerView())
two things happen. Firstly, as the size of the content element changes ScrollView animates the transition. Secondly, if you scroll down and then change the content the scrolling position does not reset.
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
The origin of this behaviour is in SwiftUI rendering optimisation, that tries to re-render only changed part, so approach is to identify view A (to mark it as completely changed) based on condition that originated in interview changes, alternatively it can be identified just by UUID().
struct TestInnerViewReplacement: View {
#State private var counter = 0
var body: some View {
VStack {
Button("Next") { self.counter += 1 }
Divider()
A(content: nextInnerView())
.id(counter) // << here !!
}
}
private func nextInnerView() -> AnyView {
AnyView(Group {
if counter % 2 == 0 {
Text("Text Demo")
} else {
Image(systemName: "star")
.resizable()
.frame(width: 100, height: 100)
}
})
}
}
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(0..<5) { _ in // upper content demo
Rectangle().fill(Color.yellow)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
content
ForEach(0..<10) { _ in // lower content demo
Rectangle().fill(Color.blue)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
}
}
}
}

SwiftUI reduce spacing of rows in a list to null

I want to reduce the linespacing in a list to null.
My tries with reducing the padding did not work.
Setting ´.environment(.defaultMinListRowHeight, 0)´ helped a lot.
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self)
{ item in
Text("\(item)")
.padding(0)
//.frame(height: 60)
.background(Color.yellow)
}
//.frame(height: 60)
.padding(0)
.background(Color.blue)
}
.environment(\.defaultMinListRowHeight, 0)
.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
}
}
}
Changing the ´separatorStyle´ to ´.none´ only removed the Line but left the space.
Is there an extra ´hidden´ view for the Lists row or for the Separator between the rows?
How can this be controlled?
Would be using ScrollView instead of a List a good solution?
ScrollView(.horizontal, showsIndicators: true)
{
//List {
ForEach(data, id: \.self)
{ item in
HStack{
Text("\(item)")
Spacer()
}
Does it also work for a large dataset?
Well, actually no surprise - .separatorStyle = .none works correctly. I suppose you confused text background with cell background - they are changed by different modifiers. Please find below tested & worked code (Xcode 11.2 / iOS 13.2)
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self)
{ item in
Text("\(item)")
.background(Color.yellow) // text background
.listRowBackground(Color.blue) // cell background
}
}
.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
}
}
}
Update:
it's not possible to avoid the blue space between the yellow Texts?
Technically yes, it is possible, however for demo it is used hardcoded values and it is not difficult to fit some, while to calculate this dynamically might be challenging... anyway, here it is
it needs combination of stack for compression, content padding for resistance, and environment for limit:
List {
ForEach(data, id: \.self)
{ item in
HStack { // << A
Text("\(item)")
.padding(.vertical, 2) // << B
}
.listRowBackground(Color.blue)
.background(Color.yellow)
.frame(height: 12) // << C
}
}
.environment(\.defaultMinListRowHeight, 12) // << D
I do it the easy SwiftUI way:
struct ContentView: View {
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
List {
ForEach(0..<10){ item in
Color.green
}
.listRowInsets( EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) )
}
}
}
Reduce row spacing is really tricky, try
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)").font(.largeTitle)
.background(Color.yellow)
}.background(Color.green)
.padding(.leading, 10)
.padding(.bottom, -25)
.frame(maxWidth: .infinity)
}
}
}
}
}
It use ScrollView instead of List and negative padding.
I didn't find any solution based on List, we have to ask Apple to publish xxxxStyle protocols and underlying structures.
UPDATE
What about this negative padding value? For sure it depends on height of our row content and unfortunately on SwiftUI layout strategy. Lets try some more dynamic content! (we use zero padding to demostrate the problem to solve)
struct ContentView: View {
#State var data : [CGFloat] = [20, 30, 40, 25, 15]
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)").font(.system(size: item))
.background(Color.yellow)
}.background(Color.green)
.padding(.leading, 10)
//.padding(.bottom, -25)
.frame(maxWidth: .infinity)
}
}
}
}
}
Clearly the row spacing is not fixed value! We have to calculate it for every row separately.
Next code snippet demonstrate the basic idea. I used global dictionary (to store height and position of each row) and tried to avoid any high order functions and / or some advanced SwiftUI technic, so it is easy to see the strategy. The required paddings are calculated only once, in .onAppear closure
import SwiftUI
var _p:[Int:(CGFloat, CGFloat)] = [:]
struct ContentView: View {
#State var data : [CGFloat] = [20, 30, 40, 25, 15]
#State var space: [CGFloat] = []
func spc(item: CGFloat)->CGFloat {
if let d = data.firstIndex(of: item) {
return d < space.count ? space[d] : 0
} else {
return 0
}
}
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)")
.font(.system(size: item))
.background(Color.yellow)
}
.background(
GeometryReader { proxy->Color in
if let i = self.data.firstIndex(of: item) {
_p[i] = (proxy.size.height, proxy.frame(in: .global).minY)
}
return Color.green
}
)
.padding(.leading, 5)
.padding(.bottom, -self.spc(item: item))
.frame(maxWidth: .infinity)
}.onAppear {
var arr:[CGFloat] = []
_p.keys.sorted(by: <).forEach { (i) in
let diff = (_p[i + 1]?.1 ?? 0) - (_p[i]?.1 ?? 0) - (_p[i]?.0 ?? 0)
if diff < 0 {
arr.append(0)
} else {
arr.append(diff)
}
}
self.space = arr
}
}
}
}
}
Running the code I've got

SwiftUI how to set image for NavigationBar titleView like in UIKit?

I want to set an image in the titleView of NavigationBar in SwiftUI, as we do in UIKit
navigationItem.titleView = UIImageView(image: UIImage(named: "logo"))
this is how we do it in UIKit.
anyone know how to do it?
Here's how to do it:
Add SwiftUIX to your project.
Set your custom title view via View.navigationBarTitleView(_:displayMode:)
Example code:
struct ContentView: View {
public var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitleView(MyView())
}
}
}
Simple, Just add your root view into ZStack with top alignment and add your custom center view after root view
struct CenterNavigattionBar: View {
var body: some View {
ZStack(alignment: .top){
//Root view with empty Title
NavigationView {
Text("Test Navigation")
.navigationBarTitle("",displayMode: .inline)
.navigationBarItems(leading: Text("Cancle"), trailing: Text("Done"))
}
//Your Custom Title
VStack{
Text("add title and")
.font(.headline)
Text("subtitle here")
.font(.subheadline)
}
}
}
}
Before Image
After Image
Just use a toolbar.
You can add any views
import SwiftUI
struct HomeView: View {
// MARK: - Initializer
init() {
let appearance = UINavigationBar.appearance()
appearance.isOpaque = true
appearance.isTranslucent = false
appearance.barTintColor = UIColor(named: "background")
appearance.shadowImage = UIImage()
}
// MARK: - View
// MARK: Public
var body: some View {
NavigationView {
VStack(spacing: 20) {
Text("Hello")
Text("Navigation Bar Test")
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarButtonItems, trailing: trailingBarButtonItems)
.toolbar {
ToolbarItem(placement: .principal) {
VStack {
Text("Title").font(.headline)
Text("Subtitle").font(.subheadline)
}
}
}
}
}
// MARK: Private
private var leadingBarButtonItems: some View {
Button(action: {
}) {
Text("Left Button")
.font(.system(size: 12, weight: .medium))
}
}
private var trailingBarButtonItems: some View {
HStack {
Button(action: {
}) {
Text("R1\nButton")
.font(.system(size: 12, weight: .medium))
.multilineTextAlignment(.center)
}
Button(action: {
}) {
Text("R2\nButton")
.font(.system(size: 12, weight: .medium))
.multilineTextAlignment(.center)
}
}
}
}
Currently, you can't.
There are two overloads for .navigationBarTitle(), taking either a Text view or a type conforming to StringProtocol. You can't even pass in a modified view like Text("Title").font(.body). This would be a great feature, I'd submit a feature request: http://feedbackassistant.apple.com
Maybe this works for you?
Basically:
Use GeometryReader to get the width of the screen
Have NavigationBarItems(leading: HStack {Spacer() Image("name").resizable().frame(width:..., height: ..., alignment: .center Spacer()}.frame(width:geometry.size.width)
Example code:
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
Text("Hello, world!")
.padding()
.navigationTitle("test")
.navigationBarItems(leading: HStack {
Spacer()
Image("money")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
Spacer()
}
.frame(width: geometry.size.width)
)
}
}
}
}
Try this...
How to put a logo in NavigationView in swiftui?
This shows how to handle adding an Image to NavigationView in SwiftUI. Hope it helps.