SwiftUI - Remove space between cells - swift

I am implementing a list view in SwiftUI. What I am trying to approach is to have cells that has no space between any other cells or parent view.
So in this screenshot as you can see there is a space between every cell and also space with the edge of the phone, which I want to remove.
struct FlickrView : View {
var flickrResponse: [FlickrResponse]
var body: some View {
List(flickrResponse) { item in
FlickrImageCell(response: item)
}
}
}
struct FlickrImageCell : View {
var response: FlickrResponse
var body: some View {
return ZStack(alignment: .topLeading) {
Image(uiImage: response.image ?? UIImage())
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: nil, height: 100.0, alignment: .center)
.clipShape(Rectangle())
.padding(0)
Text(response.title).fontWeight(.medium).multilineTextAlignment(.center)
}
}
}
I have tried this modifier:
.padding(EdgeInsets(top: 0, leading: -20, bottom: 20, trailing: -20))
But I have two problems with this approach: First, I don't think its convenient to write literal negative values. Second, the bottom padding does not work with any value.
So any suggestions?

I've had good luck with listRowInsets
struct ContentView: View {
var body: some View {
List {
Color.red
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
Color.blue
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
Color.yellow
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
}

Related

SwiftUI-How to transition between text when a button is pressed

I am making an exercise app and I want to transition between different exercises without making many seperate views as I am also making a timer which will publish too many times if I did that. I want to make it so that when I click a button, it will transition all the text from exercise1 to exercise 2 then to exercise 3 and so on. I want to transition all these using the same button.
So far what I have done is created navigation paths to different views that contain the different exercises but the timer broke when I did that because I called it many times causing it to countdown too fast. I want to see if I can contain all the exercises inside 1 view and transition between them. So for example I want to transition from:
Text(exercisePlan.exercise.title) to Text(exercisePlan.exercise2.title) then to Text(exercisePlan.exercise3.title) all with a click of one button
import SwiftUI
import AVKit
struct ExerciseScreenView: View {
#Binding var streaks: Streaks
#Binding var timerStruct: TimerStruct
var countdownTimer = 300
#State var player = AVPlayer()
var exercisePlan: ExercisePlan
#Binding var navigationPath: NavigationPath
func reset() {
timerStruct.countdownTimer = 300
timerStruct.timerRunning = false
}
var body: some View {
VStack {
Form {
Section(header: Text("1st Exercise (Scroll down for exercise steps)")) {
Text(exercisePlan.exercise.title)
.font(.system(size: 35, weight: .medium))
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
Section(header: Text("Video example")) {
VideoPlayer(player: exercisePlan.exercise.video)
.scaledToFit()
.frame(alignment: .center)
.cornerRadius(10)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
Section(header: Text("Steps:")){
Text(exercisePlan.exercise.steps)
.font(.system(size: 20, weight: .regular))
.padding()
.frame(alignment: .center)
}
}
TimerView(streaks: $streaks, timerStruct: $timerStruct)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 15, trailing: 0))
Button {
navigationPath.append("ExerciseScreen2View")
} label: {
Text("Next exercise")
.padding()
.background((Color(red: 184/255, green: 243/255, blue: 255/255)))
.foregroundColor(.black)
.cornerRadius(10)
.navigationBarBackButtonHidden()
}
.padding(EdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10))
}
}
}
What you want to do is create an array of the Exercises. Pressing the button should increase an index to get the relevant plan to show in your view.
Small code example:
struct ExerciseScreenView: View {
var exercises: [Exercise]
#State var index: 0
var body: some View {
VStack {
Text(exercises[index].title)
Button {
if exercisePlan.count - 1 > index {
index += 1
} else {
// leave screen
}
} label: {
Text("Next exercise")
}
}
}
}
}
This is a very basic example and can be improved by a lot, but it gives you a basic idea how to do it.

SwiftUI Animate Custom Progress Bar

I created this custom progress view with bars for the progress, how can I animate the the green color in the next bar when it's time to go to the next element? I am looking for it to fill the bar slowly from left to right
struct ContentView: View {
var body: some View {
ProgressBar(lastIndex: 8, currentIndex: 4)
.padding(.horizontal, 23)
.padding(.top, 10)
Spacer()
}
}
struct ProgressBar: View {
var lastIndex: Int
var currentIndex: Int
var body: some View {
HStack(alignment: .center, spacing: 4) {
ForEach(0...lastIndex, id: \.self) { i in
BarElement(selected: i <= currentIndex)
}
}
}
}
struct BarElement: View {
var selected: Bool
var body: some View {
Capsule()
.fill(selected ? .green : Color(UIColor.lightGray))
.frame(width: 33, height: 5, alignment: .center)
}
}
It's hard to answer without code but if you have all separated rectangles, it should be enough using .animation(.linear) on all the rectagles, so when the values changes it should animated as you asked.
This will do what you wanted.
struct BarElement: View {
var value : CGFloat = 0.0
var selected: Bool
var body: some View {
ZStack (alignment: .leading){
Capsule()
.fill(Color(UIColor.lightGray))
.frame(width: 33, height: 5, alignment: .leading)
.animation(.linear, value: selected)
Capsule()
.fill(.green)
.frame(width: selected ? 33 : 0, height: 5, alignment: .leading)
.animation(.linear, value: selected)
}
}
}

How to align with a certain view in a VStack?

I'm trying to achieve the following layout:
As you can see, Foo is aligned with Bar horizontally, and there's a vertical line at the bottom of Foo.
I haven't managed to do it, the best I can do is the following one:
Here is the corresponding code:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
VStack(spacing: 5) {
Text("Foo")
.padding(.bottom, 0)
getVerticalLine()
}
Text("Bar")
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
}
private func getVerticalLine() -> some View {
return Color.gray
.frame(width: 1, height: 30)
.padding(.leading, 0)
}
}
My question is: how to modify the code to achieve the expected layout?
P.S. In the end, I want to achieve something like this:
Here is the complete code that you want. You need to use multiplier for constant so that it fits in all devices.
CententView
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: true)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0))
}
}
}
FooBarView
struct FooBarView: View {
var isLastOne: Bool
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
VStack(spacing: 5) {
Text("Foo")
.padding(.bottom, 0)
}
Text("Bar")
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
if !isLastOne {
getVerticalLine()
.padding(EdgeInsets(top: -5, leading: 12, bottom: 0, trailing: 0))
}
}
}
private func getVerticalLine() -> some View {
return Color.gray
.frame(width: 1, height: 40)
.padding(.leading, 0)
}
}
Each of your activities are a single HStack, with the time on the left. The line is just something that connects each row, not necessarily part of the row itself.
You can use a ZStack to show the gray line on the back and the rows on the front; a background on the first column will avoid overlapping the gray line.
The alignment is assured by giving the same frame width to the first column (time) and the vertical line.
Here's an example:
struct Example: View {
struct Activity: Hashable, Identifiable {
let id = UUID()
let time: String
let activity: String
}
let activities = [Activity(time: "10.00", activity: "Exercise"),
Activity(time: "11.00", activity: "Work"),
Activity(time: "12.00", activity: "Study"),
Activity(time: "13.00", activity: "Coding")]
let firstColumnWidth = 80.0
let spacingBetweenLines = 30.0
let internalPadding = 10.0
let lineHeight = 20.0
var body: some View {
ZStack {
HStack {
getVerticalLine
.frame(width: firstColumnWidth)
Spacer()
}
VStack(spacing: spacingBetweenLines) {
ForEach(activities) { item in
HStack(spacing: 5) {
Text(item.time)
.frame(height: lineHeight)
.padding(internalPadding)
.frame(width: firstColumnWidth)
.background(.white)
Text(item.activity)
.frame(height: lineHeight)
.padding(internalPadding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
}
}
}
.padding()
}
private var verticalLine: some View {
let lineHeight = internalPadding * 2 + lineHeight + spacingBetweenLines
return Color.gray
.frame(width: 1, height: lineHeight * Double(activities.count - 1))
.background(.green)
}
}

How to select a Picker with MenuPickerStyle outside of the label text

I have a simple menu picker that I have placed within a rounded rectangle border and chevron image. I'd like for users to be able to tap anywhere within this rectangle to activate the showing of the picker options but no matter what it only seems to be selectable within the actual text (see image with highlighted blue border). Is there any way to achieve this without scaling the text?
I've tried adding padding and scaling modifiers with no such luck.
var body: some View {
ZStack {
HStack {
Rectangle()
.foregroundColor(Color.black)
.frame(height: 40)
Image(uiImage: Icon.chevronDown.image())
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
Picker(selectedOption, selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text(option)
}
}
.pickerStyle(MenuPickerStyle())
}
}
How can I make the white area between the rounded black border and the blue text selectable to active the menu picker options the same way as tapping on the actual blue text?
Use Picker(selection:label:content:), which takes a View as the label: argument. Put everything you want as the tappable view inside the label: argument, like so:
var body: some View {
Picker(selection: $selectedOption, label:
HStack {
Rectangle()
.foregroundColor(Color(.systemBackground))
.frame(height: 40)
Image(systemName: "chevron.down")
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.overlay(
Text("Picker Option \(selectedOption)")
)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
) {
ForEach(options, id: \.self) { option in
Text("Picker Option \(option)")
}
}
.pickerStyle(MenuPickerStyle())
}
Update: The above code doesn’t work in Xcode 13 beta 5. Hopefully it’s a bug, but if not, here is a workaround: put the Picker in a Menu!
var body: some View {
Menu {
Picker("picker", selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text("Picker Option \(option)")
}
}
.labelsHidden()
.pickerStyle(InlinePickerStyle())
} label: {
HStack {
Rectangle()
.foregroundColor(Color(.systemBackground))
.frame(height: 40)
Image(systemName: "chevron.down")
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
}
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.overlay(
Text("Picker Option \(selectedOption)")
)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
}
}

Is there a way to call a function when a SwiftUI Picker selection changes?

I would like to call a function when selectedOption's value changes. Is there a way to do this in SwiftUI similar to when editing a TextField?
Specifically, I would like to save the selected option when the user changes the selectedOption.
Here is my picker:
struct BuilderPicker: View {
let name: String
let options: Array<String>
#State var selectedOption = 0
var body: some View {
HStack {
Text(name)
.font(.body)
.padding(.leading, 10)
Picker(selection: $selectedOption, label: Text(name)) {
ForEach(0 ..< options.count) {
Text(self.options[$0]).tag($0)
}
}.pickerStyle(SegmentedPickerStyle())
.padding(.trailing, 25)
}.onTapGesture {
self.selectedOption = self.selectedOption == 0 ? 1 : 0
}
.padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
.border(Color.secondary, width: 3)
.padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
.font(.body)
}
}
I’m still new to SwiftUI and would love some help. Thanks!
If the #State value will be used in a View, you don't need extra variable name
struct BuilderPicker: View {
// let name: String = ""
let options: Array<String> = ["1", "2","3","4","5"]
#State var selectedOption = 0
var body: some View {
HStack {
Text(options[selectedOption])
.font(.body)
.padding(.leading, 10)
Picker(selection: $selectedOption, label: Text(options[selectedOption])) {
ForEach(0 ..< options.count) {
Text(self.options[$0]).tag($0)
}
}.pickerStyle(SegmentedPickerStyle())
.padding(.trailing, 25)}
// }.onTapGesture {
// self.selectedOption = self.selectedOption == 0 ? 1 : 0
// }
.padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
.border(Color.secondary, width: 3)
.padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
.font(.body)
}
}
If you need separated operation on the #State, the simplest way is adding one line : onReceive() to the view.
HStack {
Text("")
.font(.body)
.padding(.leading, 10)
Picker(selection: $selectedOption, label: Text("")) {
ForEach(0 ..< options.count) {
Text(self.options[$0]).tag($0)
}
}.pickerStyle(SegmentedPickerStyle())
.padding(.trailing, 25)}
// }.onTapGesture {
// self.selectedOption = self.selectedOption == 0 ? 1 : 0
// }
.padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
.border(Color.secondary, width: 3)
.padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
.font(.body)
.onReceive([self.selectedOption].publisher.first()) { (value) in
print(value)
}
The previous solution will end up in an infinite loop if you update an ObservedObject in the callback since .onReceive is also called when the View got rendered.
→ A better approach is to use a .onChange method on the Binding itself:
Picker(selection: $selectedOption.onChange(doSomething), label: Text("Hello world")) {
// ...
}
To do so you need to write an extension for Binding like described here.