Updated with working code.
I've implemented a List in my app with Edit mode, so I can move the rows by dragging a row handle. That works fine, but doesn't look too good, since the move icon is placed under the content of the row (see screen dump). And that is because Edit mode makes room for a delete button.
Is there a way to hide elements in the row when you're in Edit mode?
The code for the View is:
import SwiftUI
import Combine
import DateHelper
struct EggList: View {
#EnvironmentObject var egg : Egg
#State private var eggs = Egg.all()
#State private var editMode: EditMode = .inactive
var body: some View {
NavigationView {
List {
Image("Pantanal")
.resizable()
.frame(height: 250)
ForEach(eggs) { eggItem in
NavigationLink(destination: EggDayList(eggItem: eggItem)) {
CellRow(eggItem: eggItem)
.environment(\.editMode, self.$editMode)
}
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.navigationBarTitle(Text("Eggs"), displayMode: .inline)
.navigationBarItems(leading: EditButton(), trailing: NavigationLink(destination: Settings()){
Text("Add Egg")})
.environment(\.editMode, self.$editMode)
}
}
func delete(at offsets: IndexSet) {
eggs.remove(atOffsets: offsets)
}
func move(from source: IndexSet, to destination: Int) {
eggs.move(fromOffsets: source, toOffset: destination)
}
}
struct CellRow: View {
let eggItem: Egg
#Environment(\.editMode) private var editMode
var body: some View {
HStack(spacing: 8) {
Image(eggItem.species)
.resizable()
.frame(width: 48, height: 48)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 0) {
Text("\(eggItem.species)")
.font(.footnote)
.lineLimit(1)
.padding(.top, -4)
Text("id-"+String(eggItem.eggNumber))
.font(.footnote)
.lineLimit(1)
.padding(0)
Text("\(eggItem.layDate.string(with: "dd-MM-yy"))")
.font(.footnote)
.lineLimit(1)
.padding(.bottom, -7)
}.frame(width: 90, alignment: .leading)
VStack(spacing: 2) {
Text("days")
.font(.footnote)
.padding(.top, 12)
Image(systemName: "\(eggItem.diffToday)"+".circle")
.resizable()
.frame(width: 40, height: 30)
.padding(.bottom, 12)
.foregroundColor(.red)
}.frame(width: 50, alignment: .leading)
VStack(spacing: 0) {
Text("prediction")
.font(.footnote)
.padding(.top, 14)
Text(formatVar1(getal: eggItem.calcWeights[eggItem.daysToPip-1].prediction)+"%")
.font(.title)
.padding(.bottom, 12)
}.frame(width: 80, alignment: .leading)
if !(self.editMode?.wrappedValue.isEditing ?? false) {
VStack(alignment: .leading, spacing: 0) {
Text("INC")
.font(.footnote)
.lineLimit(1)
.padding(.top, -4)
Text("37.3")
.font(.footnote)
.lineLimit(1)
.padding(0)
Text("30%")
.font(.footnote)
.lineLimit(1)
.padding(.bottom, -7)
}
.frame(width: 30, alignment: .leading)
}
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Image(systemName: "info.circle")
.resizable()
.frame(width: 22, height: 22)
.foregroundColor(.accentColor)
.onTapGesture(count: 1) {
print("action")
}
}
}
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.frame(height: 46, alignment: .leading)
.padding(0)
}
}
Your problem is, that the editMode variable does not change when you press the EditButton. No matter in what state, in your CellRow the editMode variable always returns .inactive. I don't know why, it could be a bug.
I had the same problem, but I found this question here: SwiftUI - How do I make edit rows in a list?, which uses a workaround by passing the editMode environment value into a private #State variable, which seems to work perfect.
So here is what you need to do:
Add #State private var editMode: EditMode = .inactive to your EggList view.
This creates a state variable that from now on holds the editing mode.
Add .environment(\.editMode, self.$editMode) somewhere after the .navigationBarItems(...).
This sets the environment variable editMode in EggList to a binding of the state variable above.
Add .environment(\.editMode, self.$editMode) directly after the CellRow(...) initializer.
This inserts the state variable editMode into the environment of CellRow, where it can be accessed via #Environment(\.editMode).
Now you can just wrap one of your elements in an if-statement to hide it when in editing mode:
if !self.editMode?.wrappedValue.isEditing ?? true {
VStack(alignment: .leading, spacing: 0) {
Text("INC")
.font(.footnote)
.lineLimit(1)
.padding(.top, -4)
Text("37.3")
.font(.footnote)
.lineLimit(1)
.padding(0)
Text("30%")
.font(.footnote)
.lineLimit(1)
.padding(.bottom, -7)
}
.frame(width: 30, alignment: .leading)
}
or, if you prefer, continue using the isHidden extension from LuLuGaGa:
.isHidden(self.editMode?.wrappedValue.isEditing ?? false)
Add a little extension to View:
extension View {
func isHidden(_ hidden: Bool) -> some View {
if hidden {
return AnyView(self.hidden())
} else {
return AnyView(self)
}
}
}
Next add #Enviroment to your CellRow:
#Environment(\.editMode) var editMode: Binding<EditMode>?
You will be able to add a modifier:
.isHidden(editMode?.wrappedValue.isEditing ?? false)
to one of the stacks that you think is the least important.
Related
I have problem because on appear modifier doesn't call every time when I enter the screen and it calls randomly (when on appear is applied to navigation view). When I put it on some view inside navigation view it never calls. What is the problem, I can't solve it, this is very strange behaviour. This is the view:
struct CheckListView: View {
#EnvironmentObject var appState: AppState
#StateObject var checkListViewModel = CheckListViewModel()
//#State var didSelectCreateNewList = false
#State var didSelectShareList = false
var body: some View {
NavigationView {
GeometryReader { reader in
VStack {
Spacer()
.frame(height: 30)
Text("\(appState.hikingTypeModel.name)/\(appState.lengthOfStayType.name)")
.foregroundColor(Color("rectBackground"))
.multilineTextAlignment(.center)
.font(.largeTitle)
Spacer()
.frame(height: 40)
List {
ForEach(checkListViewModel.checkListModel.sections) { section in
CheckListSection(model: section)
.listRowBackground(Color("background"))
}
}
.listStyle(.plain)
.clearListBackground()
.clipped()
Spacer()
.frame(height: 40)
HStack {
FilledRectangleBorderButtonView(titleLabel: "Share", backgroundColor: Color("rectBackground"),foregroundColor: .white, height: 55, didActionOnButtonHappened: $didSelectShareList)
Spacer()
FilledRectangleBorderButtonView(titleLabel: "Create new list", backgroundColor: .clear, foregroundColor: Color("rectBackground"), height: 55, didActionOnButtonHappened: $checkListViewModel.didSelectNewList)
.onChange(of: checkListViewModel.didSelectNewList) { _ in
UserDefaultsHelper().emptyUserDefaults()
appState.moveToRootView = .createNewList
}
}
.padding([.leading, .trailing], 20)
.frame(width: reader.size.width)
Spacer()
}
.frame(width: reader.size.width, height: reader.size.height)
.background(Color("background"))
.navigationBarStyle()
}
}
.onAppear {
self.checkListViewModel.loadJSON(hikingTypeId: appState.hikingTypeModel.id, lengthStayId: appState.lengthOfStayType.id)
}
}
}
As I have constructed a list of country codes and I am able to tap on the list items. Here i want to show the tapped or selected country code inside my textfield which means, when I click on a country code it should show inside the textfield. I am new to swiftUI, it would be great if someone helped me to get this.
enter image description here
state variables given as:
#State private var text = ""
#State private var selection: String!
My textfield code goes here:
HStack {
TextField("Country code", text: $text).padding(.leading)
.frame(width: 385, height: 50)
.border(.gray)
.padding(.leading, 25)
.overlay(
Button(action: {
withAnimation {
showCodes.toggle()
}
}, label: {
Image(systemName: "chevron.down")
.foregroundColor(.gray)
.padding(.leading, 320)
.position(x: 215, y: 25)
})
)
.position(x: 195, y: 40)
.padding(.top, -570)
}
List code goes here :
if showCodes == true {
List (selection: $selection) {
ForEach(datas.users, id: \.dial_code) { user in
HStack{
Text(user.name)
.font(.callout)
.foregroundColor(Color.black)
Spacer()
Text(user.dial_code)
.font(.callout)
.foregroundColor(Color.blue)
}
}
}
.listRowInsets(EdgeInsets())
.frame(maxWidth: 400, maxHeight: .infinity, alignment: .leading)
.padding(.top, -585)
}
You can have it like:
ForEach(datas.users, id: \.dial_code) { user in
HStack {
Text(user.name)
.font(.callout)
.foregroundColor(Color.black)
Spacer()
Text(user.dial_code)
.font(.callout)
.foregroundColor(Color.blue)
}
.onTapGesture { text = user.dial_code }
}
I also suggest extracting the HStack along with it's content in a separate func like:
private func getCountryCodeCell(for user: User) -> some View {
HStack {
Text(user.name)
.font(.callout)
.foregroundColor(Color.black)
Spacer()
Text(user.dial_code)
.font(.callout)
.foregroundColor(Color.blue)
}
}
I have a VStack which wraps a number elements wrapped inside another HStack, I want to add a simple bottom to top transition on load, but it does not have any effect on the output:
var body: some View {
let bottomPadding = ScreenRectHelper.shared.bottomPadding + 150
GeometryReader { geometry in
ZStack {
VisualEffectViewSUI(effect: UIBlurEffect(style: .regular))
.edgesIgnoringSafeArea(.all)
VStack(alignment: .center) {
Spacer()
HStack {
VStack(alignment: .leading, spacing: 15) {
ForEach(items, id: \.id) { item in
HStack(alignment: .center) {
Image(item.imageName)
.resizable()
.scaledToFit()
.frame(width: 24)
Spacer()
.frame(width: 15)
Text("\(item.text)")
.foregroundColor(.white)
.transition(.move(edge: .bottom))
}
.contentShape(Rectangle())
.onTapGesture {
/// Do something
}
}
}
}
.frame(width: geometry.size.width, height: nil, alignment: .center)
}.zIndex(1)
.frame(width: geometry.size.width, height: nil, alignment: .center)
.padding(.bottom, bottomPadding)
}.background(
LinearGradient(gradient: Gradient(colors: [gradientColor.opacity(0),gradientColor]), startPoint: .top, endPoint: .bottom)
)
}.edgesIgnoringSafeArea(.all)
}
This SwiftUI view is added to a UIKit view controller and that view controller is presented modally.
.transition only works if you insert something conditionally into the view.
I'm not totally sure what you want to achieve, but if you want the whole view to slide from the bottom, this would work.
struct ContentView: View {
#State private var showText = false
let bottomPadding: CGFloat = 150
var body: some View {
ZStack {
if showText { // conditional view to make .transition work
// VisualEffectViewSUI(effect: UIBlurEffect(style: .regular))
// .edgesIgnoringSafeArea(.all)
VStack(alignment: .center) {
Spacer()
HStack {
VStack(alignment: .leading, spacing: 15) {
ForEach(0 ..< 3) { item in
HStack(alignment: .center) {
Image(systemName: "\(item).circle")
.resizable()
.scaledToFit()
.frame(width: 24)
Spacer()
.frame(width: 15)
Text("Item \(item)")
.foregroundColor(.white)
}
}
}
}
}
.frame(maxWidth: .infinity)
.padding(.bottom, bottomPadding)
.background(
LinearGradient(gradient: Gradient(colors: [.blue.opacity(0), .blue]),
startPoint: .top, endPoint: .bottom)
)
.edgesIgnoringSafeArea(.all)
.transition(.move(edge: .bottom)) // here for whole modal view
}
Button("Show modal") {
withAnimation {
showText.toggle()
}
}
}
}
}
I'm building an iOS app and currently I've found a strange behaviour with Divider component.
See the following screenshot:
The chevron on the top right side makes the other two(or more) components appear/disappear, the problem happens with the vertical Divider which should appear next to CONTROLS A.
The general SwiftUI hierarchy for the view is something similar to ScrollView -> VStack -> ForEach -> [HStack -> Img Divider VStack (with SOME TEXT + CONTROLS X)].
Note that it happens not only for the first component.
Controls X contains SwiftUI components, nothing custom.
Now some interesting facts I've found after debugging:
If I make the Divider show/disappear with a boolean flag which changes on tap, the Divider is shown as expected
If I make the Divider show/disappear with a boolean flag which changes on "onAppear" the Divider is not shown
The Divider view is included with width 0.33 but height 0 (that's the real problem)
Adding or removing views to "Controls A" can make the Divider show
Been trying to find the cause for it without success so I'm inclined to think it's probably a bug where the final height is not properly updated for the Divider component.
Update:
The issue happens only on some devices, iPhone 12 is one of them.
Here's some code to reproduce a similar issue (in this case the divider is visible but its height is wrong):
struct BugScreen: View {
var body: some View {
VStack {
Text("Bug Test")
ScrollView(showsIndicators: false) {
ForEach((1..<3)) { i in
MyView(index:i)
}
Spacer(minLength: 75)
}
.padding(.horizontal, 20)
}
.navigationBarTitle("", displayMode: .inline)
}
}
struct MyView: View {
#State var expanded = false
var index: Int
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 15) {
Image("ImageName")
.resizable()
.frame(width: 50, height: 50)
Divider()
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 5) {
HStack {
Text("Test \(index)")
.font(.headline)
.fontWeight(.light)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: expanded ? "chevron.up": "chevron.down")
.padding(.leading, 10)
.font(Font.body.weight(.thin))
}
}
Divider()
Text("Some text")
.fixedSize(horizontal: false, vertical: true)
}
}
.padding([.vertical, .leading])
.padding(.trailing, 5)
.background(
RoundedRectangle(cornerRadius: expanded ? 0: 25, style: .continuous).foregroundColor(.white)
)
.onTapGesture {
withAnimation { expanded.toggle() }
}
if expanded {
Divider().background(Color(.black))
VStack {
ForEach((1..<4)) { control in
Spacer(minLength: 10)
MyControlsView()
Divider().background(Color(.black))
}
Text("More text")
}
.padding(.horizontal, 10)
.background(Color.white)
}
Divider().background(Color(.black))
}
}
struct MyControlsView: View {
#State var sliderValue = 0.0
var body: some View {
VStack {
HStack {
Image("image_name")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
Divider()
VStack(alignment: .leading) {
Text("My controls")
Divider()
VStack(alignment: .leading) {
Slider(value: $sliderValue, in: 0...100)
Slider(value: $sliderValue, in: 0...100)
Slider(value: $sliderValue, in: 0...100)
Text("Hello")
Text("Some other text")
}
.disabled(false)
.padding(0)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(
.clear
)
)
}
}
}
.accentColor(.black)
.padding()
}
}
Found the way to make it behave as expected, just had to add .fixedSize(horizontal: false, vertical: true)
When I tapped OrderButton, all the VStack's background color is changing to gray color then turning back like assigned a click event. How can I prevent this?
import SwiftUI
struct DrinkDetail : View {
var drink: Drink
var body: some View {
List {
ZStack (alignment: .bottom) {
Image(drink.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
Rectangle()
.frame(height: 80)
.opacity(0.25)
.blur(radius: 10)
HStack {
VStack(alignment: .leading, spacing: 3) {
Text(drink.name)
.foregroundColor(.white)
.font(.largeTitle)
Text("It is just for $5.")
.foregroundColor(.white)
.font(.subheadline)
}
.padding(.leading)
.padding(.bottom)
Spacer()
}
}
.listRowInsets(EdgeInsets())
VStack(alignment: .leading) {
Text(drink.description)
.foregroundColor(.primary)
.font(.body)
.lineLimit(nil)
.lineSpacing(12)
HStack {
Spacer()
OrderButton()
Spacer()
}
.padding(.top, 50)
.padding(.bottom, 50)
}
.padding(.top)
}
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(true)
}
}
struct OrderButton : View {
var body: some View {
Button(action: {}) {
Text("Order Now")
}
.frame(width: 200, height: 50)
.foregroundColor(.white)
.font(.headline)
.background(Color.blue)
.cornerRadius(10)
}
}
#if DEBUG
struct DrinkDetail_Previews : PreviewProvider {
static var previews: some View {
DrinkDetail(drink: LoadModule.sharedInstance.drinkData[3])
}
}
#endif
The issue resolved by using ScrollView and adding some more parameters for auto resize of Text:
ScrollView(.vertical, showsIndicators: false) {
ZStack (alignment: .bottom) {
Image(drink.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
Rectangle()
.frame(height: 80)
.opacity(0.25)
.blur(radius: 10)
HStack {
VStack(alignment: .leading, spacing: 3) {
Text(drink.name)
.foregroundColor(.white)
.font(.largeTitle)
Text("It is just for $5.")
.foregroundColor(.white)
.font(.subheadline)
}
.padding(.leading)
.padding(.bottom)
Spacer()
}
}
.listRowInsets(EdgeInsets())
VStack(alignment: .leading) {
Text(drink.description)
.foregroundColor(.primary)
.font(.body)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
.lineSpacing(12)
.padding(Edge.Set.leading, 15)
.padding(Edge.Set.trailing, 15)
HStack {
Spacer()
OrderButton()
Spacer()
}
.padding(.top, 50)
.padding(.bottom, 50)
}
.padding(.top)
}
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(true)
What's the UI you are trying to get? Because, from your example, it seems that List is not necessary. Indeed, List is:
A container that presents rows of data arranged in a single column.
If you don't really need List you can replace it with VStack (if your content must fit the screen) or with a ScrollView (if your content is higher than the screen). Replacing the List will solve your issue that depends on the list selection style of its cells.
An alternative, if you need to use the List view is to set the selection style of the cells in the SceneDelegate to none (but this will affect all of your List in the project) this way:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView() //replace this with your view
if let windowScene = scene as? UIWindowScene {
UITableViewCell.appearance().selectionStyle = .none
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}