SwiftUI GeometryReader makes its content loaded twice - ios

How are you doing?
Please consider the following code:
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
ViewOne()
.frame(height: geometry.size.height / 2)
}
}
}
}
}
struct ViewOne: View {
init() {
print("View one init")
}
var body: some View {
VStack(alignment: .center) {
Text("This is View one")
.font(.system(size: 50))
}
}
}
My question is simple: Why is ViewOne built two times inside a GeometryReader and only once outside of a GeometryReader?
First, I thought that the view needed to be created once, and then a second time taking in consideration the GeometryReader sizes, however, if you have a more complex content within ViewOne, things get messy.
Any ideas??
Thanks for your time and help on this SwiftUI friends!!

It is not due to GeometryReader (consider below variant or put in into any xStack), it is because everything in placed into NavigationView, which makes own complex layout. Moreover you have not rely how many times View.init is called - View is struct, value, it can be created/copied many times during views layout & rendering (Apple promised to make it optimal, but that's it).
So, just don't put anything heavy (on unrelated) into View.init (and into View at all). Use for that other design patterns.
struct TestGeometryReaderBuilder: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
ViewOne()
.frame(height: geometry.size.height / 2)
}
}
}
}
struct ViewOne: View {
init() {
print("ViewOne> init")
}
var body: some View {
print("ViewOne> build")
return VStack(alignment: .center) {
Text("This is View one")
.font(.system(size: 50))
}
}
}

Related

I want to create a view where the hashtag is displayed in SwiftUI

Currently, I am studying SwiftUI and making UI that is displayed in the view when registering hashtags.
But I don't know what to do with the logic of creating and inserting a new HStack when it's out of screen size inside the VStack.
I've searched several times, but I couldn't find any helpful words or keywords.
I would appreciate it if you could help me by knowing the answer.
The image above is an example.
I want to create a new HStack and put it in the VStack when the width exceeds the horizontal size of the device while inserting a text item into the HStack.
struct HashTagView: View {
var hashTagArray: [String] = ["#Lorem", "#Ipsum", "#dolor", "#sit", "#amet", "#consectetur", "#adipiscing", "#elit", "#Nam", "#semper", "#sit", "#amet", "#ut", "#eleifend", "#Cras"]
var body: some View {
VStack(spacing: 5) {
ForEach(hashTagArray, id:\.self) { tag in
Text(tag)
}
}
.padding()
.border(Color.blue)
.frame(width: UIScreen.main.bounds.size.width)
}
}
this is my code.
You should use a LazyVgrid with an adaptive layout here:
I removed some entries from your array because if you use it with id: \.self you should ensure that every entry is unique.
Documentation
struct HashTagView: View {
var hashTagArray: [String] = ["#Lorem", "#Ipsum", "#dolor", "#consectetur", "#adipiscing", "#elit", "#Nam", "#semper", "#sit", "#amet", "#ut", "#eleifend", "#Cras"]
private var gridItemLayout = [GridItem(.adaptive(minimum: 100))]
var body: some View {
ScrollView{
LazyVGrid(columns: gridItemLayout , spacing: 5) {
ForEach(hashTagArray, id:\.self) { tag in
Text(tag)
}
}
.padding()
.border(Color.blue)
}
.frame(width: UIScreen.main.bounds.size.width)
}
}

Custom ScrollView Indicator in SwiftUI

Is it possible to create a custom horizontal indicator that has empty and filled circles to show how many images there are and the current position?
The below attempt uses a lazyHStack and OnAppear but, judging from the console output, it doesn't work properly since scrolling back and forth doesn't recall the onAppear consistently.
import SwiftUI
struct ContentView: View {
let horizontalScrollItems = ["wind", "hare.fill", "tortoise.fill", "rosette" ]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(horizontalScrollItems, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 200))
.frame(width: geometry.size.width)
.onAppear(){print("\(symbol)")}
}
}
}
}
}
}
This is the desired indicator. I'm just not sure how to properly fill and empty each circle as the user scrolls back and forth. Appreciate the help!
You can get the desired result using TabView() and PageTabViewStyle()
Note : This will work from SwiftUI 2.0
Here is the code :
struct ContentView: View {
let horizontalScrollItems = ["wind", "hare.fill", "tortoise.fill", "rosette" ]
var body: some View {
GeometryReader { geometry in
TabView(){
ForEach(horizontalScrollItems, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 200))
.frame(width: geometry.size.width)
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
Result :

SwiftUI | How to override life cycle methods like viewWillAppear

In my existing app, there are few UI configurations like setting up a custom navigation bar, setting a full-screen background images and some more, on all view controllers. For this purpose, configurations are made in a base view controller in viewWillAppear, all view controllers and inherited from it. So configurations are made without doing anything in child view controllers
How to achieve a similar implementation using SwiftUI?
SwiftUI provides onAppear() & onDisappear() methods on View, which is Struct and doesn't support inheritance. If I make extensions methods or protocol, I have to call methods from all Views.
Any help is appreciated...
Make your own container view. You can use #ViewBuilder to make your view behave like VStack and HStack where it takes a content closure full of subviews.
For example, here's a custom container that gives the main content a pink background, and puts a toolbar under it:
import SwiftUI
struct CustomContainer<Content: View>: View {
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
let content: Content
var body: some View {
VStack(spacing: 0) {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(hue: 0, saturation: 0.3, brightness: 1))
Toolbar()
}
}
}
struct Toolbar: View {
var body: some View {
HStack {
Spacer()
button("person.3.fill")
Spacer()
button("printer.fill")
Spacer()
button("heart.fill")
Spacer()
button("bubble.left.fill")
Spacer()
} //
.frame(height: 44)
}
private func button(_ name: String) -> some View {
Image(systemName: name)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(4)
.frame(width: 40, height: 40)
}
}
And then here's a view that uses it:
struct HelloView: View {
var body: some View {
CustomContainer {
VStack {
Text("hello")
Text("khawar")
}
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(HelloView())
Result:

Why SwiftUI context menu show all row view in preview?

I have a complex view in List row:
var body: some View {
VStack {
VStack {
FullWidthImageView(ad)
HStack {
Text("\(self.price) \(self.ad.currency!)")
.font(.headline)
Spacer()
SwiftUI.Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
Where FullWidthImageView is view with defined contexMenu modifier.
But when I long-press on an image I see not the only image in preview, but all row view.
There is no other contextMenu on any element.
How to make a preview in context with image only?
UPD. Here is a simple code illustrating the problem
We don't have any idea why in your case it doesn't work, until we see your FullWidthImageView and how you construct the context menu. Asperi's answer is working example, and it is correctly done! But did it really explain your trouble?
The trouble is that while applying .contextMenu modifier to only some part of your View (as in your example) we have to be careful.
Let see some example.
import SwiftUI
struct FullWidthImageView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
Image(systemName: model.toggle ? "pencil.and.outline" : "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
}.contextMenu(ContextMenu {
Button(action: {
self.model.toggle.toggle()
}) {
HStack {
Text("toggle image to?")
Image(systemName: model.toggle ? "trash" : "pencil.and.outline")
}
}
Button("No") {}
})
}
}
class Model:ObservableObject {
#Published var toggle = false
}
let modelStore = Model()
struct ContentView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
FullWidthImageView()
Text("Long press the image to change it").bold()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
while running, the "context menu" modified View seems to be "static"!
Yes, on long press, you see the trash image, even though it is updated properly while you dismiss the context view. On every long press you see trash only!
How to make it dynamic? I need that the image will be the same, as on my "main View!
Here we have .id modifier. Let see the difference!
First we have to update our model
class Model:ObservableObject {
#Published var toggle = false
var id: UUID {
UUID()
}
}
and next our View
FullWidthImageView().id(model.id)
Now it works as we expected.
For another example, where "standard" state / binding simply doesn't work check SwiftUI hierarchical Picker with dynamic data crashes
UPDATE
As a temporary workaround you can mimic List by ScrollView
import SwiftUI
struct Row: View {
let i:Int
var body: some View {
VStack {
Image(systemName: "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu {
Button("A") {}
Button("B") {}
})
Text("I don’t want to show in preview because I don’t have context menu modifire").bold()
}.padding()
}
}
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
ForEach(0 ..< 20) { (i) in
VStack {
Divider()
Row(i: i)
}
}
}
}
}
}
It is not optimal, but in your case it should work
Here is a code (simulated possible your scenario) that works, ie. only image is shown for context menu preview (tested with Xcode 11.3+).
struct FullWidthImageView: View {
var body: some View {
Image("auto")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu() {
Button("Ok") {}
})
}
}
struct TestContextMenu: View {
var body: some View {
VStack {
VStack {
FullWidthImageView()
HStack {
Text("100 $")
.font(.headline)
Spacer()
Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
}
}
}
}
It's buried in the replies here, but the key discovery is that List is changing the behavior of .contextMenu -- it creates "blocks" that pop up with the menu instead of attaching the menu to the element specified. Switching out List for ScrollView fixes the issue.

ForEach inside ScrollView doesn't take whole width

I'm trying to re-create UI of my current app using SwiftUI. And it is way more difficult than I initially though.
I wanted to achieve card-like cells with some background behind them. I found that List doesn't support that, at least yet. List is so limited - it doesn't allow you to remove cell separator.
So I moved to ForEach inside ScrollView. I guess that isn't something which should be used in production for long tables but that should work for now. The problem I have is that ForeEach view doesn't take all the width ScrollView provides. I can set .frame(...) modifier but that will require hardcoding width which I definitely don't want to do.
Any ideas how to force VStack take full width of the ScrollView? I tried to use ForeEach without VStack and it has the same issue. It seems like ScrollView (parent view) "tells" its child view (VStack) that its frame is less that actual ScrollView's frame. And based on that information child views build their layout and sizes.
Here is my current result:
And here is the code:
struct LandmarkList : View {
var body: some View {
NavigationView {
ScrollView() {
VStack {
Spacer().frame(height: 160)
ForEach(landmarkData) { landmark in
LandmarkRow(landmark: landmark).padding([.leading, .trailing], 16)
}
}.scaledToFill()
.background(Color.pink)
}
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(landmark.name).font(.title)
Text("Subtitle")
.font(.callout)
.color(.gray)
}
Spacer()
Text("5 mi")
.font(.largeTitle)
}.frame(height: 80)
.padding()
.background(Color.white)
.cornerRadius(16)
.clipped()
.shadow(radius: 2)
}
}
I've got the same issue, the only way I have found so far is to fix the ScrollView and the content view width, so that every subview you add inside the content view will be centered.
I created a simple wrapper that take the width as init parameter
struct CenteredList<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable {
public private(set) var width: Length
private var data: Data
private var contentBuilder: (Data.Element.IdentifiedValue) -> Content
init(
width: Length = UIScreen.main.bounds.width,
data: Data,
#ViewBuilder content: #escaping (Data.Element.IdentifiedValue) -> Content)
{
self.width = width
self.data = data
self.contentBuilder = content
}
var body: some View {
ScrollView {
VStack {
ForEach(data) { item in
return self.contentBuilder(item)
}.frame(width: width)
}
.frame(width: width)
}
.frame(width: width)
}
}
By default it takes the screen width (UIScreen.main.bounds.width).
It works just like a List view:
var body: some View {
TileList(data: 0...3) { index in
HStack {
Text("Hello world")
Text("#\(index)")
}
}
}
Its possible that the answer to this might just be wrapping your scrollView inside of a GeometryReader
Like done in the answer here -> How do I stretch a View to its parent frame with SwiftUI?

Resources