Creating Baseview Class in Swiftui

Creating BaseView class in SwiftUI

Usually you want to either have a common behaviour or a common style.

1) To have a common behaviour: composition with generics

Let's say we need to create a BgView which is a View with a full screen image as background. We want to reuse BgView whenever we want. You can design this situation this way:

struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
let content: Content

var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}

You can use BgView wherever you need it and you can pass it all the content you want.

//1
struct ContentView: View {
var body: some View {
BgView(content: Text("Hello!"))
}
}

//2
struct ContentView: View {
var body: some View {
BgView(content:
VStack {
Text("Hello!")
Button(action: {
print("Clicked")
}) {
Text("Click me")
}
}
)
}
}

2) To have a common behaviour: composition with @ViewBuilder closures

This is probably the Apple preferred way to do things considering all the SwiftUI APIs. Let's try to design the example above in this different way

struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
private let content: Content

public init(@ViewBuilder content: () -> Content) {
self.content = content()
}

var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}

struct ContentView: View {
var body: some View {
BgView {
Text("Hello!")
}
}
}

This way you can use BgView the same way you use a VStack or List or whatever.

3) To have a common style: create a view modifier

struct MyButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
.shadow(radius: 3)
}
}

struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Button(action: {
print("Button1 clicked")
}) {
Text("Button 1")
}
.modifier(MyButtonStyle())

Button(action: {
print("Button2 clicked")
}) {
Text("Button 2")
}
.modifier(MyButtonStyle())

Button(action: {
print("Button3 clicked")
}) {
Text("Button 3")
}
.modifier(MyButtonStyle())
}
}
}

These are just examples but usually you'll find yourself using one of the above design styles to do things.

EDIT: a very useful link about @functionBuilder (and therefore about @ViewBuilder) https://blog.vihan.org/swift-function-builders/

SwiftUI best way to create base view that will be inherited

How about using a custom View Modifier. It gets content passed in and you don't need extra stacks:

struct ContentView: View {

@State var showSideMenu: Bool = false

var body: some View {
VStack{
Text("HOME")
Button(action: {
withAnimation {
self.showSideMenu.toggle()
}
}) {
Text("Show Menu")
}
}
.baseViewModifier(showSideMenu: $showSideMenu)
}
}


extension View {
func baseViewModifier(showSideMenu: Binding<Bool>) -> some View {
self.modifier(BaseViewModifier(showSideMenu: showSideMenu))
}
}

struct BaseViewModifier: ViewModifier {

@Binding var showSideMenu: Bool

func body(content: Content) -> some View {

content
.frame(maxWidth:.infinity, maxHeight: .infinity)
.offset(x: showSideMenu ? UIScreen.main.bounds.width / 1.5 : 0)
.disabled(showSideMenu ? true : false)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
}
}

How to use BaseView @State property in SubView SwiftUI

If view contains BaseView it is definitely not a subview relating to it. And you should not access/manipulate internal state of other view due to single-source-of-truth violation.

Instead you have to use Binding in this scenario, like below (tested with Xcode 11.7)

struct BaseView<Content: View>: View {
@Binding var isAlertPresented: Bool
let content: Content

init(isAlertPresented: Binding<Bool>, @ViewBuilder content: () -> Content) {
self._isAlertPresented = isAlertPresented
self.content = content()
}

var body : some View {
content.alert(isPresented: $isAlertPresented) {
Alert(title: Text("title"))
}
}

}

struct SomeOtherView: View {
@State private var showAlert = false

var body: some View {
BaseView(isAlertPresented: $showAlert) {
Text("Test")
}
.onReceive(vm.publisher) { (output) in
// .onAppear { // used instead of above just for test
self.showAlert = true
}
}
}

Subclass-like behaviour for wrapped SwiftUI views

The solution I ended up with was removing the whole AsyncImage concept. After all, the ImageDownloader is likely gonna use some shared global state (e.g. a manager somewhere), so hiding it within AsyncImage was not a good choice. Better explicit than implicit :)

Instead, I moved the downloader one level upstream:

struct UserDetailsView: View {
@ObservedObject var imageDownloader: ImageDownloader

var body: some View {
VStack {
Image(uiImage: imageDownloader.image).resizable().aspectRatio(16.0 / 9.0, contentMode: .fit)
Text(user.name)
}
}
}

This approach has multiple advantages:

  • direct access to all Image modifiers
  • downloader can be furthermore injected from upstream
  • reusability wasn't affected - instead of reusing AsyncImage, I reuse ImageDownloader

My take from this: trying to make reusable components by mixing business and presentation code is not a good approach. Single Responsibility Principle for the win :)

How to show/reuse the same view (Alert) in SwiftUI from all ViewModels in case of an error/message?

You have to update myTest property wrapper and forcefully call Base-class objectwillchange publisher.

@Published var myTest : String = "Hello" {
willSet {
self.objectWillChange.send()
}
}

Note: @Published property wrapper is not working incase of inherited class.

I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.

SwiftUI : I get lost when I try to pass EnvironmentObject to another View when using MVVM

You have to inject your dataModel from the root of you app, that’s from Main root struct.

import SwiftUI

struct Payer: Identifiable {
var id = UUID().uuidString
var name : String = "ABCD"

var offset : CGFloat = 0
}

class BaseViewModel: ObservableObject {

//* The problem * //
@Published var payer: Payer
//* The problem * //

@Published var payers = [
Payer( name: "AA"),
Payer( name: "BaB"),
Payer( name: "CC"),
]

init(payer: Payer) {

self.payer = payer
}
}

struct ContentView: View {
@EnvironmentObject var vm : BaseViewModel
var body: some View {
Text(vm.payer.name)
}
}

@Main-:

import SwiftUI

@main
struct WaveViewApp: App {

@StateObject var dataController:BaseViewModel

init() {
let payer = Payer()
let model = BaseViewModel(payer:payer)
_dataController = StateObject(wrappedValue: model)
}

var body: some Scene {
WindowGroup {
ContentView().environmentObject(dataController)
}
}
}


Related Topics



Leave a reply



Submit