Difference between creating ViewModifier and View extension in SwiftUI - ios

I'm trying to find out what is practical difference between these two approaches. For example:
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
extension View {
func makePrimaryLabel() -> some View {
self
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
Then we can use all of them following way:
Text(tech.title)
.modifier(PrimaryLabel())
Text(tech.title)
.makePrimaryLabel()
ModifiedContent(
content: Text(tech.title),
modifier: PrimaryLabel()
)

All of the approaches you mentioned are correct. The difference is how you use it and where you access it. Which one is better? is an opinion base question and you should take a look at clean code strategies and SOLID principles and etc to find what is the best practice for each case.
Since SwiftUI is very modifier chain base, The second option is the closest to the original modifiers. Also you can take arguments like the originals:
extension Text {
enum Kind {
case primary
case secondary
}
func style(_ kind: Kind) -> some View {
switch kind {
case .primary:
return self
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
case .secondary:
return self
.padding()
.background(Color.blue)
.foregroundColor(Color.red)
.font(.largeTitle)
.cornerRadius(20)
}
}
}
struct ContentView: View {
#State var kind = Text.Kind.primary
var body: some View {
VStack {
Text("Primary")
.style(kind)
Button(action: {
self.kind = .secondary
}) {
Text("Change me to secondary")
}
}
}
}
We should wait and see what is the BEST practices in new technologies like this. Anything we find now is just a GOOD practice.

I usually prefer extensions, as they get you a more readable code and they are generally shorter to write. I wrote an article about View extensions.
However, there are differences. At least one. With ViewModifier you can have #State variables, but not with View extensions. Here's an example:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, how are you?").modifier(ColorChangeOnTap())
}
}
}
struct ColorChangeOnTap: ViewModifier {
#State private var tapped: Bool = false
func body(content: Content) -> some View {
return content.foregroundColor(tapped ? .red : .blue).onTapGesture {
self.tapped.toggle()
}
}
}

I believe the best approach is combining ViewModifiers and View extension. This will allow composition of #State within the ViewModifier and convenience of View extension.
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
extension View {
func makePrimaryLabel() -> some View {
ModifiedContent(content: self, modifier: PrimaryLabel())
}
}
Usage
Text(tech.title)
.makePrimaryLabel()

It may be that there is an advantage in the type signature of the resulting Views when using a ViewModifier. For example if we create the following TestView to display the types of the three variants using this:
struct TestView: View {
init() {
print("body1: \(type(of: body))")
print("body2: \(type(of: body2))")
print("body3: \(type(of: body3))")
}
#ViewBuilder var body: some View {
Text("Some Label")
.modifier(PrimaryLabel())
}
#ViewBuilder var body2: some View {
Text("Some Label")
.makePrimaryLabel()
}
#ViewBuilder var body3: some View {
ModifiedContent(
content: Text("Some Label"),
modifier: PrimaryLabel()
)
}
}
We can see it yields the following types:
body1: ModifiedContent<Text, PrimaryLabel>
body2: ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<Text, _PaddingLayout>, _BackgroundStyleModifier<Color>>, _EnvironmentKeyWritingModifier<Optional<Color>>>, _EnvironmentKeyWritingModifier<Optional<Font>>>, _ClipEffect<RoundedRectangle>>
body3: ModifiedContent<Text, PrimaryLabel>
Even if there isn't an advantage during execution it might make debugging a little easier if nothing else.

There is another approach: using View extension and a generic custom view. Using generic custom view resolves the issue that #kontiki mentioned (how to apply it to other views). Below is the code:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, how are you?").colorChangeOnTap()
}
}
}
struct ColorChangeOnTap<Content: View>: View {
var content: Content
#State private var tapped: Bool = false
var body: some View {
return content.foregroundColor(tapped ? .red : .blue).onTapGesture {
self.tapped.toggle()
}
}
}
extension View {
func colorChangeOnTap() -> some View {
ColorChangeOnTap(content: self)
}
}
While being different, the approach is very similar to the view modifier approach. I suspect this might be what the SwiftUI team originally had and, when they added more features to it, it evolved into view modifier.

Related

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

SwiftUI - reusable components with links to other views as parameters

I would like to create reusable components in my app.
I have searched for similar problem. But I have only found much more complex examples.
Let's try this simple example - a button that could open different Views based on passed parameter.
I have 2 views that I will open as a sheet:
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("First view")
}
}
SecondView.swift
struct SecondView: View {
var body: some View {
Text("Second view")
}
}
ButtonView.swift
This is a view I would like to use as a reusable component in my design system.
import SwiftUI
struct ButtonView: View {
#State private var showModal: Bool = false
// This works
var text: String
// Here I am getting an error:
// Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
var link: View
var body: some View {
VStack {
Spacer()
Button(action: {
self.showModal = true
}) {
Text(text)
.padding(20)
.foregroundColor(Color.white)
}.sheet(isPresented: self.$showModal) {
link
}
.background(Color.blue)
}
}
}
struct ButtonView_Previews: PreviewProvider {
static var previews: some View {
ButtonView(text: "TEST", link: FirstView())
}
}
ContentView.swift Here I am trying to use the same button component, but with different labels and links.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
ButtonView(text: "first", link: FirstView())
.padding()
ButtonView(text: "second", link: SecondView())
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Passing String parameters works. Labels are different. But I cannot make it work with links to different Views. I am getting an error:
Protocol 'View' can only be used as a generic constraint because it
has Self or associated type requirements
Keeping First view and Second View as the same, use the following for the ButtonView:
struct ButtonView<Content : View>: View {
#State private var showModal: Bool = false
var text: String
// This is the generic content parameter
let content: Content
init(text: String, #ViewBuilder contentBuilder: () -> Content){
self.text = text
self.content = contentBuilder()
}
var body: some View {
VStack {
Spacer()
Button(action: {
self.showModal = true
}) {
Text(text)
.padding(20)
.foregroundColor(Color.white)
}.sheet(isPresented: self.$showModal) {
content
}
.background(Color.blue)
}
}
}
Here the generic parameter named content is used to receive any view and the initializer is used with the #ViewBuilder property wrapper to build the view.Now use it in the following way in ContentView struct:
struct ContentView: View {
var body: some View {
HStack {
ButtonView(text: "First") {
FirstView()
}
ButtonView(text: "Second") {
SecondView()
}
}
}
}
It will work like a charm :)
Also if you want to keep preview for ButtonView and don't want it to crash then add the preview as:
struct ButtonView_Previews: PreviewProvider {
static var previews: some View {
ButtonView(text: "First") {
FirstView()
}
}
}

Create a view over navigationView, SwiftUI

I'm creating a view embedded in a ZStack, something like this:
ZStack(alignment: .top) {
content
if self.show {
VStack {
HStack {
This is a viewModifier so I call this in my main view with for example: .showView().
But what happened is that if I have a NavigationView, this view is only showing below the navigationView. (I have a navigationViewTitle that is over my view).
How can I solve this problem? I was thinking about some zIndex but it is not working. I thought also about some better placement of this .showView(), but nothing to do.
Here is a demo of possible approach (it can be added animations/transitions, but it is out of topic). Demo prepared & tested with Xcode 11.4 / iOS 13.4
struct ShowViewModifier<Cover: View>: ViewModifier {
let show: Bool
let cover: () -> Cover
func body(content: Content) -> some View {
ZStack(alignment: .top) {
content
if self.show {
cover()
}
}
}
}
struct DemoView: View {
#State private var isPresented = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Link", destination: Button("Details")
{ self.isPresented.toggle() })
Text("Some content")
.navigationBarTitle("Demo")
Button("Toggle") { self.isPresented.toggle() }
}
}
.modifier(ShowViewModifier(show: isPresented) {
Rectangle().fill(Color.red)
.frame(height: 200)
})
}
}

SwiftUI: Custom button does not recognize touch with clear background and buttonStyle

I stumbled upon a weird behaviour for Buttons in SwiftUI in combination with a custom ButtonStyle.
My target was to create a custom ButtonStyle with some kind of 'push-back animation'. I used the following setup for this:
struct CustomButton<Content: View>: View {
private let content: () -> Content
init(content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
Button(action: { ... }) {
content()
}
.buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
}
}
}
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
// Preview
struct Playground_Previews: PreviewProvider {
static var previews: some View {
CustomButton {
VStack(spacing: 10) {
HStack {
Text("Button Text").background(Color.orange)
}
Divider()
HStack {
Text("Detail Text").background(Color.orange)
}
}
}
.background(Color.red)
}
}
When I now try to touch on this button outside of the Text view, nothing will happen. No animation will be visible and the action block will not be called.
What I found out so far:
when you remove the .buttonStyle(...) it does work as expected (no custom animation of course)
or when you set a .background(Color.red)) on the VStack in the CustomButton it does also work as expected in combination with the .buttonStyle(...)
The question now is if anybody have a better idea of how to properly work around this issue or how to fix it?
Just add hit testing content shape in your custom button style, like below
Tested with Xcode 11.4 / iOS 13.4
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.contentShape(Rectangle()) // << fix !!
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
Simply use a .frame and it should work.
To make it easily testable I have rewritten it like this:
struct CustomButton: View {
var body: some View {
Button(action: { }) {
VStack(spacing: 10) {
HStack {
Text("Button Text").background(Color.orange)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.orange)
}
Divider()
HStack {
Text("Detail Text").background(Color.orange)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.orange)
}
}
}
.buttonStyle(PushBackButtonStyle(pushBackScale: 0.9))
}
}
private struct PushBackButtonStyle: ButtonStyle {
let pushBackScale: CGFloat
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.scaleEffect(configuration.isPressed ? pushBackScale : 1.0)
}
}
I hope I could help. :-)
#Edit With video.

How to check if a view is displayed on the screen? (Swift 5 and SwiftUI)

I have a view like below. I want to find out if it is the view which is displayed on the screen. Is there a function to achieve this?
struct TestView: View {
var body: some View {
Text("Test View")
}
}
You could use onAppear on any kind of view that conforms to View protocol.
struct TestView: View {
#State var isViewDisplayed = false
var body: some View {
Text("Test View")
.onAppear {
self.isViewDisplayed = true
}
.onDisappear {
self.isViewDisplayed = false
}
}
func someFunction() {
if isViewDisplayed {
print("View is displayed.")
} else {
print("View is not displayed.")
}
}
}
PS: Although this solution covers most cases, it has many edge cases that has not been covered. I'll be updating this answer when Apple releases a better solution for this requirement.
You can check the position of view in global scope using GeometryReader and GeometryProxy.
struct CustomButton: View {
var body: some View {
GeometryReader { geometry in
VStack {
Button(action: {
}) {
Text("Custom Button")
.font(.body)
.fontWeight(.bold)
.foregroundColor(Color.white)
}
.background(Color.blue)
}.navigationBarItems(trailing: self.isButtonHidden(geometry) ?
HStack {
Button(action: {
}) {
Text("Custom Button")
} : nil)
}
}
private func isButtonHidden(_ geometry: GeometryProxy) -> Bool {
// Alternatively, you can also check for geometry.frame(in:.global).origin.y if you know the button height.
if geometry.frame(in: .global).maxY <= 0 {
return true
}
return false
}
As mentioned by Oleg, depending on your use case, a possible issue with onAppear is its action will be performed as soon as the View is in a view hierarchy, regardless of whether the view is potentially visible to the user.
My use case is wanting to lazy load content when a view actually becomes visible. I didn't want to rely on the view being encapsulated in a LazyHStack or similar.
To achieve this I've added an extension onBecomingVisible to View that has the same kind of API as onAppear, but will only call the action when the view intersects the screen's visible bounds.
public extension View {
func onBecomingVisible(perform action: #escaping () -> Void) -> some View {
modifier(BecomingVisible(action: action))
}
}
private struct BecomingVisible: ViewModifier {
#State var action: (() -> Void)?
func body(content: Content) -> some View {
content.overlay {
GeometryReader { proxy in
Color.clear
.preference(
key: VisibleKey.self,
// See discussion!
value: UIScreen.main.bounds.intersects(proxy.frame(in: .global))
)
.onPreferenceChange(VisibleKey.self) { isVisible in
guard isVisible else { return }
action?()
action = nil
}
}
}
}
struct VisibleKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) { }
}
}
Discussion
I'm not thrilled by using UIScreen.main.bounds in the code! Perhaps a geometry proxy could be used for this instead, or some #Environment value – I've not thought about this yet though.

Resources