My scrollview with vStack worked (in AppleTV - i did not test in iOS) without GeometryReader.
Then i added the geometryreader and the VStack "collapses" like a ZStack.
What can i do to solve this? (and yes, i need the geometryreader ;))
struct ContentView: View {
#State var offset : CGFloat = 0
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
GeometryReader { geometry -> AnyView in
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
print("new offset",newOffset)
}
return AnyView (
ForEach (0..<5) { _ in
Text("umpf")
}
)
}
}
}
}
}
Result:
and this code works as desired:
struct ContentView: View {
#State var offset : CGFloat = 0
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
// GeometryReader { geometry -> AnyView in
// let newOffset = geometry.frame(in: .global).minY
// if newOffset != self.offset {
// self.offset = newOffset
// print("new offset",newOffset)
// }
// return AnyView (
ForEach (0..<5) { _ in
Text("umpf")
}
// )
// }
}
}
}
}
result:
Let me suppose that initially you wanted this (and it does not break ScrollView layout & scrolling)
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
ForEach (0..<5) { _ in
Text("umpf")
}
}.background(
GeometryReader { geometry -> Color in
let newOffset = geometry.frame(in: .global).minY
if newOffset != self.offset {
self.offset = newOffset
print("new offset",newOffset)
}
return Color.clear
}
)
}
}
Related
I'm trying to use a LazyVGrid in SwiftUI where you can touch and drag your finger to select multiple adjacent cells in a specific order. This is not a drag and drop and I don't want to move the cells (maybe drag isn't the right term here, but couldn't think of another term to describe it). Also, you would be able to reverse the selection (ie: each cell can only be selected once and reversing direction would un-select the cell). How can I accomplish this? Thanks!
For example:
struct ContentView: View {
#EnvironmentObject private var cellsArray: CellsArray
var body: some View {
VStack {
LazyVGrid(columns: gridItems, spacing: spacing) {
ForEach(0..<(rows * columns), id: \.self){index in
VStack(spacing: 0) {
CellsView(index: index)
}
}
}
}
}
}
struct CellsView: View {
#State var index: Int
#EnvironmentObject var cellsArray: CellsArray
var body: some View {
ZStack {
Text("\(self.cellsArray[index].cellValue)") //cellValue is a string
.foregroundColor(Color.yellow)
.frame(width: getWidth(), height: getWidth())
.background(Color.gray)
}
//.onTapGesture ???
}
func getWidth()->CGFloat{
let width = UIScreen.main.bounds.width - 10
return width / CGFloat(columns)
}
}
Something like this might help, the only issue here is the coordinate space. Overlay is not drawing the rectangle in the correct coordinate space.
struct ImagesView: View {
var columnSize: CGFloat
#Binding var projectImages: [ProjectImage]
#State var selectedImages: [ImageSelection] = []
#State var dragWidth: CGFloat = 1.0
#State var dragHeight: CGFloat = 1.0
#State var dragStart: CGPoint = CGPoint(x:0, y:0)
let columns = [
GridItem(.adaptive(minimum: 200), spacing: 0)
]
var body: some View {
GeometryReader() { geometry in
ZStack{
ScrollView{
LazyVGrid(columns: [
GridItem(.adaptive(minimum: columnSize), spacing: 2)
], spacing: 2){
ForEach(projectImages, id: \.imageUUID){ image in
Image(nsImage: NSImage(data: image.imageData)!)
.resizable()
.scaledToFit()
.border(selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? Color.blue : .primary, width: selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? 5.0 : 1.0)
.gesture(TapGesture(count: 2).onEnded{
print("Double tap finished")
})
.gesture(TapGesture(count: 1).onEnded{
if selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) {
print("Image is already selected")
if let index = selectedImages.firstIndex(where: {imageSelection in imageSelection.uuid == image.imageUUID}){
selectedImages.remove(at: index)
}
} else {
selectedImages.append(ImageSelection(imageUUID: image.imageUUID))
print("Image has been selected")
}
})
}
}
.frame(minHeight: 50)
.padding(.horizontal, 1)
}
.simultaneousGesture(
DragGesture(minimumDistance: 2)
.onChanged({ value in
print("Drag Start: \(value.startLocation.x)")
self.dragStart = value.startLocation
self.dragWidth = value.translation.width
self.dragHeight = value.translation.height
})
)
.overlay{
Rectangle()
.frame(width: dragWidth, height: dragHeight)
.offset(x:dragStart.x, y:dragStart.y)
}
}
}
}
}
I have a scrollview who's content expands to fill the screen and stacks in front using the zIndex. The expanded content also has a scrollview for the content inside. I'm literally trying to mimic the Apps Stores "Today" tab with expanding cards and scrollable content inside.
The way I built this though I realized the expanding view is still part of the parent scrollview. As a result the scrollviews are nested and conflict. This was not what I intended.
Im very new to programming. This is the code that basically expands each card. Its pretty basic. A ternary expands the cards. The CardView is the cards content. Attached is a screenshot of what I'm trying to achieve. Need help here. Any info would be great. Been searching the internet for a way to do this right.
struct Media: View {
#EnvironmentObject var vm : ViewModel
#Environment(\.colorScheme) var mode
#State var showCard = false
#State var activeCard = -1
let height = UIScreen.main.bounds.height
var body: some View {
ZStack {
ScrollView (.vertical, showsIndicators: false) {
VStack (alignment:.center, spacing: 30) {
ForEach (vm.cards.indices, id: \.self) { i in
let z=vm.cards[i].z
GeometryReader { geom in
ZStack {
CardView (showCard: $showCard,
activeCard: self.$activeCard,
zValue: $vm.cards[i].z,
i: i,
cards: vm.cards[i])
}//Z
.frame(minHeight: showCard && activeCard == i ? height : nil) //Animates Card Scaling
.padding(.horizontal, showCard && activeCard == i ? 0:20) // Card Padding
.offset(y: i==activeCard ? -geom.frame(in: .global).minY : 0)
} //GEOM
.frame(minHeight: 450)
.zIndex(z)
} //LOOP
} //V
.padding([.bottom, .top])
} //SCROLLVIEW
} //Z
.background(Color("Background Gray"))
}
}
I don't see a problem with the nested ScrollViews, as you put the DetailView on top. I tried to rebuild a simplified version from your code, see below.
BTW: You don't need the outer ZStack.
And at some point you should consider using ForEach over the cards, not over the indices, otherwise you'll run into UI update problems when inserting or deleting cards. I left it for now to stay closer to your original.
struct ContentView: View {
#State var activeCard = -1
let height = UIScreen.main.bounds.height
var body: some View {
ScrollView (.vertical, showsIndicators: false) {
VStack (alignment:.center, spacing: 30) {
ForEach (data.indices, id: \.self) { i in
GeometryReader { geom in
ZStack {
DetailCellView(entry: data[i], isActive: activeCard == i)
.onTapGesture {
if activeCard == i {
activeCard = -1
} else {
activeCard = i
}
}
}//Z
.frame(minHeight: activeCard == i ? height : nil) //Animates Card Scaling
.padding(.horizontal, activeCard == i ? 0 : 20) // Card Padding
.offset(y: activeCard == i ? -geom.frame(in: .global).minY : 0)
} //GEOM
.frame(minHeight: 450)
.zIndex(activeCard == i ? 1 : 0)
.animation(.default, value: activeCard)
} //LOOP
} //V
// .padding([.bottom, .top])
} //SCROLLVIEW
.background(.gray)
}
}
for the while being: a custom outer dragview as workaround ...
struct ContentView: View {
#State var activeCard = -1
let height = UIScreen.main.bounds.height
#State private var offset = CGFloat.zero
#State private var drag = CGFloat.zero
var body: some View {
GeometryReader { geo in
VStack (alignment:.center, spacing: 30) {
ForEach (data.indices, id: \.self) { i in
GeometryReader { geom in
ZStack {
DetailCellView(entry: data[i],
isActive: activeCard == i)
.onTapGesture {
if activeCard == i {
activeCard = -1
} else {
activeCard = i
}
}
}
.frame(minHeight: activeCard == i ? height : nil) //Animates Card Scaling
.padding(.horizontal, activeCard == i ? 0 : 20) // Card Padding
.offset(y: activeCard == i ? -geom.frame(in: .global).minY : 0)
}
.frame(minHeight: 400)
.zIndex(activeCard == i ? 1 : 0)
.animation(.default, value: activeCard)
}
}
// custom drag view
.offset(x: 0 , y: offset + drag)
.background(.gray)
// drag
.gesture(DragGesture()
.onChanged({ value in
drag = value.translation.height
})
.onEnded({ value in
print(offset)
withAnimation(.easeOut) {
offset += value.predictedEndTranslation.height
offset = min(offset, 0)
offset = max(offset, -CGFloat(data.count * (400 + 30)) + height )
drag = 0
}
print(offset)
})
)
}
}
}
So I figured out how to accomplish what I was trying to accomplish.
struct ScrollingHelper: UIViewRepresentable {
let proxy: ScrollingProxy // reference type
func makeUIView(context: Context) -> UIView {
return UIView() // managed by SwiftUI, no overloads
}
func updateUIView(_ uiView: UIView, context: Context) {
proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}
class ScrollingProxy {
private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func disableScrolling(_ flag: Bool) {
scrollView?.isScrollEnabled = flag
print(flag)
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
I used the above code from https://stackoverflow.com/a/60855853/12299030 &
Disable Scrolling in SwiftUI List/Form
Then used in a TapGesture
scrollEnabled = false
scrollProxy.disableScrolling(scrollEnabled)
and used background modifier:
.background(ScrollingHelper(proxy: scrollProxy))
I am building a Picker with SwiftUI.
Now i want do add an icon AND text for each selection. So it should look something like this:
Is this possible? If yes how to do it?
Or is it not recommended by Apples apples human interface guidelines at all?
I already tried to use a HStack to wrap image and text together.
enum Category: String, CaseIterable, Identifiable {
case person
case more
var id: String { self.rawValue }
}
struct ContentView: View {
#State private var category = Category.person
var body: some View {
Picker("Category", selection: $category) {
HStack {
Image(systemName: "person")
Text("Person")
}.tag(Category.person)
HStack {
Image(systemName: "ellipsis.circle")
Text("More")
}.tag(Category.more)
}.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
But the framework splits it up into four.
You can make a custom Picker
struct ContentView: View {
var body: some View {
Home()
}
}
struct Home: View {
#State var index = 0
var body: some View {
VStack {
HStack {
Text("Picker with icon")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.black)
Spacer(minLength: 0)
}
.padding(.horizontal)
HStack(spacing: 0){
HStack{
Image(systemName: "person")
.foregroundColor(self.index == 0 ? .black : .gray)
Text("Person")
.foregroundColor(self.index == 0 ? .black : .gray)
}
.padding(.vertical, 10)
.padding(.horizontal, 35)
.background((Color.white).opacity(self.index == 0 ? 1 : 0))
.clipShape(Capsule())
.onTapGesture {
self.index = 0
}
HStack{
Image(systemName: "ellipsis.circle")
.foregroundColor(self.index == 1 ? .black : .gray)
Text("More")
.foregroundColor(self.index == 1 ? .black : .gray)
}
.padding(.vertical, 10)
.padding(.horizontal, 35)
.background((Color.white).opacity(self.index == 1 ? 1 : 0))
.clipShape(Capsule())
.onTapGesture {
self.index = 1
}
}
.padding(3)
.background(Color.black.opacity(0.06))
.clipShape(Capsule())
Spacer(minLength: 0)
}
.padding(.top)
}
}
This is a way using Apple Picker with the output you want:
enum Category: String, CaseIterable, Identifiable {
case person
case more
var id: String { self.rawValue }
}
struct ContentView: View {
#State private var category = Category.person
private var view1: some View { HStack { Image(systemName: "person"); Text("Person") } }
private var view2: some View { HStack { Image(systemName: "ellipsis.circle"); Text("More") } }
#State private var uiImage1: UIImage? = nil
#State private var uiImage2: UIImage? = nil
var body: some View {
return Picker("Category", selection: $category) {
if let unwrappedUIImage1 = uiImage1 {
Image(uiImage: unwrappedUIImage1)
.tag(Category.person)
}
if let unwrappedUIImage2 = uiImage2 {
Image(uiImage: unwrappedUIImage2)
.tag(Category.more)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
.onAppear() {
DispatchQueue.main.async {
uiImage1 = viewToUIImageConverter(content: view1)
uiImage2 = viewToUIImageConverter(content: view2)
}
print("Your selection is:", category.rawValue)
}
.onChange(of: category, perform: { newValue in print("Your selection is:", newValue.rawValue) })
}
}
func viewToUIImageConverter<Content: View>(content: Content) -> UIImage? {
let controller = UIHostingController(rootView: content)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = UIColor.clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
I am having an issue getting the ContentBodyView to update properly when a button is pressed in the MenuView.
When I print out the #Published currentPage var, it changes on button press, but doesn't appear to get carried across into ContentBodyView. Am I missing something?
I wrote the code based off of this tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
I've tried doing an .onReceive event on ContentBodyView, but the view still did not change.
App.swift:
#main
struct App: App {
#StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewRouter)
}
}
}
ContentView:
struct ContentView: View {
// MARK: - PROPERTIES
// for future use...
#State var width = UIScreen.main.bounds.width - 90
// to hide view...
#State var x = -UIScreen.main.bounds.width + 90
#EnvironmentObject var viewRouter: ViewRouter
// MARK: - BODY
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) {
ContentBodyView(x: $x)
.environmentObject(ViewRouter())
MenuView(x: $x)
.shadow(color: Color.black.opacity(x != 0 ? 0.1 : 0), radius: 5, x: 5, y: 0)
.offset(x: x)
.background(Color.black.opacity(x == 0 ? 0.5 : 0).ignoresSafeArea(.all, edges: .vertical).onTapGesture {
// hiding the view when back is pressed...
withAnimation {
x = -width
}
}) //: background
.environmentObject(ViewRouter())
} //: ZSTACK
// adding gesture or drag feature...
.gesture(DragGesture().onChanged({ (value) in
withAnimation {
if value.translation.width > 0 {
// disabling over drag...
if x < 0 {
x = -width + value.translation.width
}
} else {
if x != -width {
x = value.translation.width
}
}
}
}).onEnded({ (value) in
withAnimation {
// checking if half the value of menu is dragged means setting x to 0...
if -x < width / 2 {
x = 0
} else {
x = -width
}
}
})) //: GESTURE
} //: VSTACK
}
}
MenuView:
struct MenuView: View {
// MARK: - PROPERTIES
var edges = UIApplication.shared.windows.first?.safeAreaInsets
// for future use...
#State var width = UIScreen.main.bounds.width - 90
// to hide view...
#Binding var x: CGFloat
#EnvironmentObject var viewRouter: ViewRouter
// MARK: - BODY
var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading) {
HStack{
Button(action: {
withAnimation {
x = -width
}
}) {
Image(systemName: "xmark")
.resizable()
.frame(width: 18, height: 18)
.padding()
.padding(.top, 25)
.foregroundColor(Color.black)
}
Spacer()
}
ForEach(menuData) { item in
Button(action: {
withAnimation {
if (item.router == "shareables") {
viewRouter.currentPage = .shareables
} else {
viewRouter.currentPage = .home
}
x = -width
}
}) {
Text("\(item.label)")
} //: BUTTON
} //: FOREACH
} //: VSTACK
.padding(.horizontal,20)
// since vertical edges are ignored....
.padding(.top,edges!.top == 0 ? 15 : edges?.top)
.padding(.bottom,edges!.bottom == 0 ? 15 : edges?.bottom)
// default width...
.frame(width: UIScreen.main.bounds.width - 90)
.background(Color.white)
.ignoresSafeArea(.all, edges: .vertical)
Spacer(minLength: 0)
} //: HSTACK
}
}
ContentBodyView:
struct ContentBodyView: View {
// MARK: - PROPERTIES
#EnvironmentObject var viewRouter: ViewRouter
#Binding var x : CGFloat
// MARK: - BODY
var body: some View{
VStack {
switch viewRouter.currentPage {
case .home:
NavigationBarView(x: $x, title: "Home")
Spacer()
HomeView()
.transition(.scale)
case .shareables:
NavigationBarView(x: $x, title: "Shareables")
Spacer()
ShareablesView()
.transition(.scale)
} //: SWITCH
} //: VSTACK
// for drag gesture...
.contentShape(Rectangle())
.background(Color.white)
}
}
ViewRouter:
final class ViewRouter: ObservableObject {
#Published var currentPage: Page = .home
}
enum Page {
case home
case shareables
}
Try using the same ViewRouter instance in all views.
The following code creates new instances of ViewRouter:
ContentBodyView(x: $x)
.environmentObject(ViewRouter())
MenuView(x: $x)
...
.environmentObject(ViewRouter())
Replace them with:
#EnvironmentObject var viewRouter: ViewRouter
...
.environmentObject(viewRouter)
But in reality you usually need to inject .environmentObject only once per environment, so all these calls may be unnecessary.
Injecting the ViewRouter once in ContentView should be enough (if you're not using sheet etc):
#StateObject var viewRouter = ViewRouter()
...
ContentView().environmentObject(viewRouter)
I want to let my cell looks not fill in list's column. I have already clear the list background color and
separatorStyle set .none. I also set my cellView's listRowBackground been gray, but it doesn't work well.The background color is still white in my cell. How do I clear my list's column background color? Please help. Thank you.
struct TeamListView: View {
#EnvironmentObject var userToken : UserToken
#State var teamResults : [TeamResult] = []
var body: some View {
NavigationView {
ZStack{
Color.gray.edgesIgnoringSafeArea(.all)
VStack {
List(teamResults) { team in
TeamListCellView(teamResult: team)
}.navigationBarTitle(Text("My team"),displayMode: .inline)
}
}
.onAppear(perform: {
self.getTeamData()
UITableView.appearance().backgroundColor = .gray
UITableView.appearance().separatorStyle = .none
})
.onDisappear(perform: {
UITableView.appearance().backgroundColor = .white
UITableView.appearance().separatorStyle = .singleLine
})
}
Below is my cellView, I set the .listRowBackground(Color.gray) in here.
struct TeamListCellView: View {
// #ObservedObject var teamResult: TeamResult
var teamResult: TeamResult
var body: some View {
NavigationLink(destination: TeamDetail(teamResult1: teamResult)) {
Image(uiImage: teamResult.teamImage)
.resizable()
.aspectRatio(contentMode: ContentMode.fill)
.frame(width:70, height: 70)
.cornerRadius(35)
VStack(alignment: .leading) {
Text(teamResult.groupName)
Text(teamResult.groupIntro)
.font(.subheadline)
.foregroundColor(Color.gray)
}
} .frame(width:200,height: 100)
.background(Color.green)
.cornerRadius(10)
.listRowBackground(Color.gray)
}
}
You can create a Background<Content: View> and use it to set the background colour of your view. To do it you can embed your views inside your Background View
For example:
struct ContentView: View {
#EnvironmentObject var userToken : UserToken
#State var teamResults : [TeamResult] = []
var body: some View {
Background{
NavigationView {
ZStack{
Color.gray.edgesIgnoringSafeArea(.all)
VStack {
List(teamResults) { team in
TeamListCellView(teamResult: team)
}
.navigationBarTitle(Text("My team"),displayMode: .inline)
}
}
.onAppear(perform: {
self.getTeamData()
UITableView.appearance().backgroundColor = .gray
UITableView.appearance().separatorStyle = .none
})
.onDisappear(perform: {
UITableView.appearance().backgroundColor = .white
UITableView.appearance().separatorStyle = .singleLine
})
}
}
}
}
struct Background<Content: View>: View {
private var content: Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
Color.gray
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.overlay(content)
}
}