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 reuseImageDownloader
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
How to Generate a Binding for Each Array Element
Nsjsonserialization Not Working as Expected in a Playground
Arkit -Drop a Shadow of 3D Object on the Plane Surface
How to Convert Anyclass to a Specific Class and Init It Dynamically in Swift
How to Change Navigationbar Font in Swift
Using Scenekit in Swift Playground
Expand and Contract Tableview Cell When Tapped, in Swift
Get Lat and Long from Tapped Overlay in Google Maps
Nothing Prints Out in the Console in Command Line Tool Xcode
Default Optional Parameter in Swift Function
What Is the Swift Equivalent of -[Nsobject Description]
Lazy Property Initialization in Swift
Split Uint32 into [Uint8] in Swift
Building a Spritekit/Gamekit Leaderboard Within a Specific Scene
How to Assign an Array to a Class Property by Reference Rather Than a Copy