SwiftUI: using MatchedGeometryEffect for a scrollView? - swift

I am trying to understand MatchGeometryEffect a little better. I am trying to put it on each cell of a list in a scrollView. I want it to be where if showAlternate is false, the main cell is show but if showAlternate is true , then the individual cell will show the else of the conditional which would be a larger cell ( not part of the code yet). I have the matchedGeometryEffect on the ForEach right now but how can I do each cell indvidually?
struct ContentView: View {
#Binding var countries: [Country]
#State var showAlternate = false;
#Namespace var namespace;
var body: some View {
NavigationView {
ScrollView {
ForEach(Array(countries.enumerated()), id: \.1.id) { (index,country) in
LazyVStack {
HStack {
NavigationLink(
destination: CountryView(country: country),
label: {
HStack {
Image(country.image)
.resizable()
.scaledToFit()
Text(country.display_name)
.foregroundColor(Color.black)
.padding(.leading)
Spacer()
}
.padding(.top, 12.0)
.frame(height: 80)
}
).buttonStyle(FlatLinkStyle())
}
.padding(.horizontal, 16.0)
.overlay(Divider(), alignment: .top)
}
}
.background(Rectangle().opacity(0.2).matchedGeometryEffect(id: "shape", in: namespace))
}
}
.font(Font.custom("Avenir", size: 22))
}
}

Related

Combining 2 ScrollViews in Swift

I'm trying to connect 2 views. One is a ScrollView and the other is a List. Does anyone know how to do that? Putting both into a singular ScrollView doesn't work, and calling APIResponseView().environmentObject(Model()) inside of ContentView() also doesn't work.
struct MyApp: App {
var body: some Scene {
WindowGroup {
VStack{
ContentView() // this is a ScrollView
APIResponseView().environmentObject(Model()) // this is a List
}
}
}
}
As you can see, the views aren't connected:
Image found here
If you want to have the ScrollView move out of view when scrolling the list, you have to put the ScrollView INSIDE the list:
struct ContentView: View {
#State private var test = false
var body: some View {
// List
List {
// ScrollView
ScrollView(.horizontal) {
HStack {
ForEach(0..<6) { i in
Text("ScrollView \(i)")
.frame(width: 150, height: 200)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
)
}
}
}
ForEach(0..<10) { item in
HStack {
VStack {
Text("Overheadline")
.font(.caption)
.foregroundColor(.secondary)
Text("List Item \(item)")
}
Spacer()
RoundedRectangle(cornerRadius: 10)
.fill(.gray)
.frame(width: 50, height: 50)
}
}
}
.listStyle(.plain)
}
}

How do I fix buttons not being tappable in a ForEach loop?

I have a view which contains a button. The view with the button is created from a forEach loop. For some reason only some buttons are tappable and others are not.
The parent view contains a NavigationView, a scroll view inside the NavigationView, a lazyVStack inside of the scroll view, a forEachloop in that lazyVStack and in that for loop is the child view that contains the button.
struct ContentView: View {
let peoples:[Person] = Bundle.main.decode("data.json")
var body: some View {
let columns = [
GridItem(.flexible(minimum: 300), spacing: 10)
]
NavigationView {
ScrollView(.vertical) {
LazyVStack {
ForEach(peoples, id: \.self) { person in
PersonView(name: person.Name, age: person.Age)
}
}
.navigationTitle("A list of people")
.navigationViewStyle(DefaultNavigationViewStyle())
.padding()
}
}
}
}
The child view is bellow. I suspect the scroll view is stealing the user input, but I am not sure why or how to overcome it. Some buttons are tapeable and some are not.
struct PersonView: View {
#Environment(\.colorScheme) var colorScheme
var name: String
var age: Int
var body: some View {
VStack(alignment:.leading) {
Image("randoPerson")
.resizable()
.scaledToFill()
.frame(minWidth: nil, idealWidth: nil,
maxWidth: UIScreen.main.bounds.width, minHeight: nil,
idealHeight: nil, maxHeight: 300, alignment: .center)
.clipped()
VStack(alignment: .leading, spacing: 6) {
Text("name")
.fontWeight(.heavy)
.padding(.leading)
Text("Age \(age)")
.foregroundColor(Color.gray)
.padding([.leading,.bottom])
Button(action: { print("I was tapped") }) {
HStack {
Image(systemName: "message.fill")
.font(.title)
.foregroundColor(.white)
.padding(.leading)
Text("Message them")
.font(.subheadline)
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
}
.padding()
}
.background(Color(UIColor.systemBackground).cornerRadius(15))
.shadow(color:colorScheme == .dark
? Color.white.opacity(0.2)
: Color.black.opacity(0.2),
radius: 7, x: 0, y: 2)
}
}
}
To fix the issue, you can add an id: UUID associated to a Person and iterate between the Person, inside the ForEach using their ID.
You will also noticed that I added the value as lowercases to respect the Swift convention.
struct Person {
let id: UUID // Add this value
var name: String
var age: Int
}
So here is the ContentView with the id replacing \.self:
struct ContentView: View {
let peoples: [Person] = Bundle.main.decode("data.json")
var body: some View {
NavigationView {
ScrollView(.vertical) {
LazyVStack {
ForEach(peoples, id: \.id) { person in // id replace \.self here
PersonView(name: person.name, age: person.age) // removed uppercase
}
}
.navigationTitle("A list of people")
.navigationViewStyle(DefaultNavigationViewStyle())
.padding()
}
}
}
}

iOS 14 SwiftUI Keyboard lifts view automatically

I am using TextField in my view and when it becomes the first responder, it lefts the view as shown in the below GIF.
Is there any way I can get rid of this behavior?
Here is my code
NavigationView(content: {
ZStack{
MyTabView(selectedIndex: self.$index)
.view(item: self.item1) {
NewView(title: "Hello1").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item2) {
NewView(title: "Hello2").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item3) {
NewView(title: "Hello3").navigationBarTitle("")
.navigationBarHidden(true)
}
}.navigationBarHidden(true)
.navigationBarTitle("")
}).ignoresSafeArea(.keyboard, edges: .bottom)
// New View
struct NewView:View {
#State var text:String = ""
var title:String
var body: some View {
VStack {
Spacer()
Text("Hello")
TextField(title, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.onAppear {
debugPrint("OnApper \(self.title)")
}
}
}
For .ignoresSafeArea to work you need to fill all the available area (eg. by using a Spacer).
The following will not work (no Spacers, just a TextField):
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("asd", text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
However, it will work when you add Spacers (fill all the available space):
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
Spacer()
TextField("asd", text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
If you don't want to use Spacers you can also use a GeometryReader:
struct ContentView: View {
#State var text: String = ""
var body: some View {
GeometryReader { _ in
...
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
You should apply the modifier on the ZStack, NOT the NavigationView
NavigationView(content: {
ZStack{
,,,
}.navigationBarHidden(true)
.navigationBarTitle("")
.ignoresSafeArea(.keyboard, edges: .bottom) // <- This line moved up
})
Full working example:
struct ContentView: View {
#State var text = ""
var body: some View {
VStack{
Spacer()
Text("Hello, World")
TextField("Tap to test keyboard ignoring", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
What eventually worked for me, combining answers posted here and considering also this question, is the following (Xcode 12.4, iOS 14.4):
GeometryReader { _ in
VStack {
Spacer()
TextField("Type something...", text: $value)
Spacer()
}.ignoresSafeArea(.keyboard, edges: .bottom)
}
Both spacers are there to center vertically the textfield.
Using only the GeometryReader or the ignoresSafeArea modifier didn't do the trick, but after putting them all together as shown above stopped eventually the view from moving up upon keyboard appearance.
That's what I figured out:
GeometryReader { _ in
ZStack {
//PUT CONTENT HERE
}.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
It seems to work for me. In this case you do not need to check iOS 14 availability.

Adding Segmented Style Picker to SwiftUI's NavigationView

The question is as simple as in the title. I am trying to put a Picker which has the style of SegmentedPickerStyle to NavigationBar in SwiftUI. It is just like the native Phone application's history page. The image is below
I have looked for Google and Github for example projects, libraries or any tutorials and no luck. I think if nativa apps and WhatsApp for example has it, then it should be possible. Any help would be appreciated.
SwiftUI 2 + toolbar:
struct DemoView: View {
#State private var mode: Int = 0
var body: some View {
Text("Hello, World!")
.toolbar {
ToolbarItem(placement: .principal) {
Picker("Color", selection: $mode) {
Text("Light").tag(0)
Text("Dark").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
}
You can put a Picker directly into .navigationBarItems.
The only trouble I'm having is getting the Picker to be centered. (Just to show that a Picker can indeed be in the Navigation Bar I put together a kind of hacky solution with frame and Geometry Reader. You'll need to find a proper solution to centering.)
struct ContentView: View {
#State private var choices = ["All", "Missed"]
#State private var choice = 0
#State private var contacts = [("Anna Lisa Moreno", "9:40 AM"), ("Justin Shumaker", "9:35 AM")]
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.contacts, id: \.self.0) { (contact, time) in
ContactView(name: contact, time: time)
}
.onDelete(perform: self.deleteItems)
}
.navigationBarTitle("Recents")
.navigationBarItems(
leading:
HStack {
Button("Clear") {
// do stuff
}
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.frame(width: 130)
.pickerStyle(SegmentedPickerStyle())
.padding(.leading, (geometry.size.width / 2.0) - 130)
},
trailing: EditButton())
}
}
}
func deleteItems(at offsets: IndexSet) {
contacts.remove(atOffsets: offsets)
}
}
struct ContactView: View {
var name: String
var time: String
var body: some View {
HStack {
VStack {
Image(systemName: "phone.fill.arrow.up.right")
.font(.headline)
.foregroundColor(.secondary)
Text("")
}
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
Text("iPhone")
.foregroundColor(.secondary)
}
Spacer()
Text(self.time)
.foregroundColor(.secondary)
}
}
}
For those who want to make it dead center, Just put two HStack to each side and made them width fixed and equal.
Add this method to View extension.
extension View {
func navigationBarItems<L, C, T>(leading: L, center: C, trailing: T) -> some View where L: View, C: View, T: View {
self.navigationBarItems(leading:
HStack{
HStack {
leading
}
.frame(width: 60, alignment: .leading)
Spacer()
HStack {
center
}
.frame(width: 300, alignment: .center)
Spacer()
HStack {
//Text("asdasd")
trailing
}
//.background(Color.blue)
.frame(width: 100, alignment: .trailing)
}
//.background(Color.yellow)
.frame(width: UIScreen.main.bounds.size.width-32)
)
}
}
Now you have a View modifier which has the same usage of navigationBatItems(:_). You can edit the code based on your needs.
Usage example:
.navigationBarItems(leading: EmptyView(), center:
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}, trailing: EmptyView())
UPDATE
There was the issue of leading and trailing items were violating UINavigationBarContentView's safeArea. While I was searching through, I came across another solution in this answer. It is little helper library called SwiftUIX. If you do not want install whole library -like me- I created a gist just for navigationBarItems. Just add the file to your project.
But do not forget this, It was stretching the Picker to cover all the free space and forcing StatusView to be narrower. So I had to set frames like this;
.navigationBarItems(center:
Picker(...) {
...
}
.frame(width: 150)
, trailing:
StatusView()
.frame(width: 70)
)
If you need segmentcontroll to be in center you need to use GeometryReader, below code will provide picker as title, and trailing (right) button.
You set up two view on the sides left and right with the same width, and the middle view will take the rest.
5 is the magic number depends how width you need segment to be.
You can experiment and see the best fit for you.
GeometryReader {
Text("TEST")
.navigationBarItems(leading:
HStack {
Spacer().frame(width: geometry.size.width / 5)
Spacer()
picker
Spacer()
Button().frame(width: geometry.size.width / 5)
}.frame(width: geometry.size.width)
}
But better solution is if you save picker size and then calculate other frame sizes, so picker will be same on ipad & iphone
#State var segmentControllerWidth: CGFloat = 0
var body: some View {
HStack {
Spacer()
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
.background(Color.red)
segmentController
.fixedSize()
.background(PreferenceViewSetter())
profileButton
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
}
.onPreferenceChange(PreferenceViewKey.self) { preferences in
segmentControllerWidth = preferences.width
}
}
struct PreferenceViewSetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: PreferenceViewKey.self,
value: PreferenceViewData(width: geometry.size.width))
}
}
}
struct PreferenceViewData: Equatable {
let width: CGFloat
}
struct PreferenceViewKey: PreferenceKey {
typealias Value = PreferenceViewData
static var defaultValue = PreferenceViewData(width: 0)
static func reduce(value: inout PreferenceViewData, nextValue: () -> PreferenceViewData) {
value = nextValue()
}
}
Simple answer how to center segment controller and hide one of the buttons.
#State var showLeadingButton = true
var body: some View {
HStack {
Button(action: {}, label: {"leading"})
.opacity(showLeadingButton ? true : false)
Spacer()
Picker(selection: $selectedStatus,
label: Text("SEGMENT") {
segmentValues
}
.id(UUID())
.pickerStyle(SegmentedPickerStyle())
.fixedSize()
Spacer()
Button(action: {}, label: {"trailing"})
}
.frame(width: UIScreen.main.bounds.width)
}

Views compressed by other views in SwiftUI VStack and List

In my SwiftUI application, I'm trying to implement a UI similar to this:
I've added the two rows for category 1 and category 2. The result looks like this:
NavigationView {
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
Spacer()
}
.navigationBarTitle(Text("Featured"))
}
Now, when added the view for the third category – an VStack with images – the following happens:
This happened, after I replaced Spacer(), with said VStack:
VStack(alignment: .leading) {
Text("Rivers")
.font(.headline)
ForEach(self.categories["Rivers"]!.identified(by: \.self)) { landmark in
landmark.image(forSize: 200)
}
}
My CategoryRow is implemented as follows:
VStack(alignment: .leading) {
Text(title)
.font(.headline)
ScrollView {
HStack {
ForEach(landmarks) { landmark in
CategoryItem(landmark: landmark, isRounded: self.isRounded)
}
}
}
}
Question
It seems that the views are compressed. I was not able to find any compression resistance or content hugging priority modifiers to fix this.
I also tried to use .fixedSize() and .frame(width:height:) on CategoryRow.
How can I prevent the compression of these views?
Update
I've tried embedding the whole outer stack view in a scroll view:
NavigationView {
ScrollView { // also tried List
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
ForEach(...) { landmark in
landmark.image(forSize: 200)
}
}
.navigationBarTitle(Text("Featured"))
}
}
...and the result is worse:
You might prevent the views in VStack from being compressed by using
.fixedSize(horizontal: false, vertical: true)
For example:
I have the following VStack:
VStack(alignment: .leading){
ForEach(group.items) {
FeedCell(item: $0)
}
}
Which render compressed Text()
When I add .fixedSize(horizontal: false, vertical: true)
it doesn't compress anymore
VStack(alignment: .leading){
ForEach(group.items) {
FeedCell(item: $0)
.fixedSize(horizontal: false, vertical: true)
}
}
You could try to add a layoutPriority()operator to your first VStack. This is what the documentation says about the method:
In a group of sibling views, raising a view’s layout priority encourages that view to shrink later when the group is shrunk and stretch sooner when the group is stretched.
So it's a bit like the content compression resistance priority in Autolayout. But the default value here is 0, so you just have to set it to 1 to get the desired effect, like this:
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
Spacer()
}.layoutPriority(1)
VStack(alignment: .leading) {
...
}
Hope it works!
It looks like is not enough space for all your views in VStack, and it compresses some of them. You can embed it into the ScrollView
NavigationView {
ScrollView {
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
/// you images and so on
}
}
}
struct ContentView1: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
CategoryListView {
CategoryView()
}
CategoryListView {
SquareCategoryView()
}
CategoryListView {
RectangleCategoryView()
}
}
.padding()
}
.navigationTitle("Featured")
}
}
}
struct CategoryListView<Content>: View where Content: View {
private let viewSize: CGFloat = 150
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
HStack {
Text("Category name")
Spacer()
}
ScrollView(.horizontal, showsIndicators: false){
HStack {
ForEach(0..<10) { _ in
content()
}
}
}
}
}
}
struct ContentView1_Previews: PreviewProvider {
static var previews: some View {
ContentView1()
}
}
struct CategoryView: View {
private let viewSize: CGFloat = 150
var body: some View {
Circle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize)
}
}
struct RectangleCategoryView: View {
private let viewSize: CGFloat = 350
var body: some View {
Rectangle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize * 9 / 16)
}
}
struct SquareCategoryView: View {
private let viewSize: CGFloat = 150
var body: some View {
Rectangle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize)
}
}
I think your topmost view (in the NavigationView) needs to be a List, so that it is scrollable:
NavigationView {
List {
...
Or use a ScrollView.
A stack automatically fits within a screen. If you want your content to exceed this, you would have used a ScrollView or a TableView etc i UIKit
EDIT:
Actually, a little Googling brought this result, which seems to be exactly what you are making:
https://developer.apple.com/tutorials/swiftui/composing-complex-interfaces