SwiftUI | How to override life cycle methods like viewWillAppear - ios

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:

Related

SwiftUI ScrollView extra padding when go to another screen with showed keyboard

The default "Keyboard Avoidance" SwiftUI is used.
First GIF
If you put the code in VStack and add another View to it, then the container rises
Second GIF
I don't want to delete Keyboard Avoidance. I need to remove extra spacing
scrollDismissesKeyboard for ScrollView is not an option
minimal iOS version is iOS 16
struct ContentView: View {
#State var text: String = "Bu bu?"
var body: some View {
NavigationStack {
ScrollViewReader { proxy in
VStack {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
Spacer()
.frame(height: 500)
TextField("", text: $text)
.padding(.bottom, 70)
.frame(height: 40)
.frame(maxWidth: .infinity)
.background(Color.red)
NavigationLink("Screen 2", destination: {
Text("SwiftUI - Nice to meet you, let's never meet again")
})
}
}
Text("I'm in VSTack after scroll view")
}
}
}
}
}
I looked it up with a hierarchy view, and noticed that a UIInputSetHostView is created with a height of 216
View hierarchy 1
View hierarchy 2
disableAutocorrection not working

Create a common layout for the navigation bar in SwiftUI, so other SwiftUI views should reuse same Nav Bar

In iOS SwiftUI, how can we make a common layout for the navigation bar, so we can use that in all projects without rewriting the same code?
We can use ViewBuilder to create a base view for common code as follows:
struct BaseView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
// To-do: The most important part will go here
}
}
How can we add navigation bar code in View Builder or base view?
One way to achieve this is to use a custom view as an overlay.
For example, consider the below code which makes a custom navigation bar using an overlay:
struct Home: View {
var body: some View {
ScrollView {
// Your Content
}
.overlay {
ZStack {
Color.clear
.background(.ultraThinMaterial)
.blur(radius: 10)
Text("Navigation Bar")
.font(.largeTitle.weight(.bold))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 20)
}
.frame(height: 70)
.frame(maxHeight: .infinity, alignment: .top)
}
}
}
The ZStack inside the .overlay will make a view that looks like a navigation bar. You can then move it to its own struct view, add different variables to it and call it from the overlay.
You can create an extension of view like this way. You can check out my blog entry for details.
import SwiftUI
extension View {
/// CommonAppBar
public func appBar(title: String, backButtonAction: #escaping() -> Void) -> some View {
self
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
backButtonAction()
}) {
Image("ic-back") // set backbutton image here
.renderingMode(.template)
.foregroundColor(Colors.AppColors.GrayscaleGray2)
})
}
}
Now you can use this appBar in any place of the view.
struct TransactionsView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
VStack(spacing: 0) {
}
.appBar(title: "Atiar Talukdar") {
self.mode.wrappedValue.dismiss()
}
}
}
struct TransactionsView_Previews: PreviewProvider {
static var previews: some View {
TransactionsView()
}
}

How to stop SwiftUI change the fixed padding when we switch button content between Text and Image?

So I have two buttons on the bottom of my screen, button A and button B, somewhere along the line I need to replace the image in the button with text, so we do so by changing the Bool in the top.
Although we apply the same modifiers, the padding of button B changes, and the UI moves around, it seems as if the text claims more space. Desired situation: button A and B, should not move around when changing the button Image to Text.
import SwiftUI
private var showImage: Bool = true
struct SwiftUIView: View {
var body: some View {
VStack {
Spacer()
Button(action: {
print("CLICK")
}) {
Image(systemName: "a.circle")
.modifier(TestButtonModifier())
}
.padding(10)
Button(action: {
print("CLICK")
}) {
if showImage {
Image(systemName: "b.circle")
.modifier(TestButtonModifier())
} else {
Text("B")
.modifier(TestButtonModifier())
}
}
.padding(10)
} //: VSTACK
}
}
struct TestButtonModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 52, weight: .regular))
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 100)
.background(Color.black)
.padding(2)
.foregroundColor(Color.white)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
It is due to VStack spacing, which by default differs between different pairs of subviews, so specify some explicit (or remove at all, ie set to zero)
struct SwiftUIView: View {
var body: some View {
VStack(spacing: 0) { // << here !!
// .. other code

SwiftUI GeometryReader makes its content loaded twice

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))
}
}
}

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