Context
I am currently working on a system for handling Sheets in SwiftUI. However, I encountered a problem when utilizing the MVVM Design Pattern and generics.
I have a Sheet struct containing the generic View the Sheet should display. I also have an #Published variable in the view model holding the currently active Sheet.
However, obviously, this does not allow sheets with different view types, since I get the following compiler errors:
Error 1: Reference to generic type 'Sheet' requires arguments in <...>
Error 2: 'nil' requires a contextual type
Code
public struct Sheet<Content: View>: Identifiable {
public let id = UUID()
let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
}
public class SheetViewModel: ObservableObject {
public static let shared = SheetViewModel()
private init() {}
#Published var sheet: Sheet? // Error 1
public func present<Content: View>(_ sheet: Sheet<Content>) {
self.sheet = sheet
}
public func present<Content: View>(#ViewBuilder content: () -> Content) {
self.sheet = CRSheet(content: content)
}
public func dismiss() {
self.sheet = nil // Error 2
}
}
public struct SheetViewModifier: ViewModifier {
#ObservedObject private var sheetVM = SheetViewModel.shared
public func body(content: Content) -> some View {
content
.sheet(item: $sheetVM.sheet) { sheet in
sheet.content
}
}
}
Question
How can I resolve the errors, especially, be able to store sheets with different generic views inside the same #Published Variable and later use them inside the view modifier?
My idea was to maybe store the View as any View inside the sheet Struct`. However, I am not sure, how to use it inside the view modifier then?
Please Note: This code is part of a package and therefore needs to be accessed from outside. The generic content therefore must be passed to the present(_) Method.
Ok, so if I'm understanding this correctly, you want to handle the logic for showing different sheets from the Generic View.
First, let's build the GenericView:
struct GenericView<Content: View, FirstSheet: View, SecondSheet: View>: View {
private enum ModalSheet: Identifiable {
var id: Self { return self }
case firstSheet
case secondSheet
}
#State private var showModalSheet: ModalSheet?
private let content: () -> Content
private let firstSheet: () -> FirstSheet
private let secondSheet: () -> SecondSheet
public init(
#ViewBuilder content: #escaping () -> Content,
#ViewBuilder firstSheet: #escaping () -> FirstSheet,
#ViewBuilder secondSheet: #escaping () -> SecondSheet
) {
self.content = content
self.firstSheet = firstSheet
self.secondSheet = secondSheet
}
var body: some View {
ZStack {
content()
VStack {
Button("Show first sheet") {
showModalSheet = .firstSheet
}
Button("Show second sheet") {
showModalSheet = .secondSheet
}
}
}
.sheet(item: $showModalSheet) { modalSheet in
switch modalSheet {
case .firstSheet:
firstSheet()
case .secondSheet:
secondSheet()
}
}
}
}
Note that I created an enum inside GenericView to handle the logic for the different sheets
So now, if I want to use the generic view inside a regular view, I would do the following:
struct ContentView: View {
var body: some View {
GenericView {
Color.yellow.ignoresSafeArea()
} firstSheet: {
Text("Content for First Sheet")
} secondSheet: {
Text("Content for Second Sheet")
}
}
}
This way you can control which sheet you present as well as dismissing them without using a ViewModel.
Is this more or less what you had in mind?
Related
I am facing an issue when trying to initialize a view in SwiftUI. Let me explain:
I have a view called ExternalView, this view has a constraint with a protocol called ToggleableProtocol, then, this view internally consumes a view called InternalView, which is a view that has a constraint to a Hashable protocol.
The error occurs when I try to create an instance of ExternalView and pass it an array of different structs that conform to the ToggleableItem protocol. The error says Type 'any TogleableItem' cannot conform to 'TogleableItem'
TogleableItem
public protocol TogleableItem: Hashable {
var text: String { get }
var isSelected: Bool { get set }
}
Structs conforming to the TogleableItem
struct FirstObject: TogleableItem {
var text = "FirstItem"
var isSelected = false
}
struct SecondObject: TogleableItem {
var text = "SecondItem"
var isSelected = false
}
ContentView
struct ContentView: View {
#State private var items: [any TogleableItem] = [
FirstObject(),
SecondObject()
]
var body: some View {
ExternalView(
items: items
) { isSelected, index, _ in
items[index].isSelected = isSelected
}
}
}
ExternalView
public struct ExternalView<T: TogleableItem>: View {
private let items: [T]
private let onItemTap: (Bool, Int, T) -> Void
public init(
items: [T],
onItemTap: #escaping (Bool, Int, T) -> Void
) {
self.items = items
self.onItemTap = onItemTap
}
public var body: some View {
InternalView(
items: items
) { index, element in
Text(element.text)
.foregroundColor(element.isSelected ? .red : .blue)
}
}
}
InternalView
struct InternalView<Element: Hashable, Content: View>: View {
private let items: [Element]
private let content: (Int, Element) -> Content
init(
items: [Element],
content: #escaping (Int, Element) -> Content
) {
self.items = items
self.content = content
}
var body: some View {
LazyHStack(spacing: 0) {
ForEach(items.indices, id: \.self) { index in
content(index, items[index])
}
}
}
}
Error
Thanks!!
I have tried changing the items parameter, inside the ExternalView to something like [any TogleableItem] but in the end it causes a similar error when consuming the InternalView
The problem with your code is that you define for instance ExternalView as generic
ExternalView<T: TogleableItem>
then you say that T can be of a (read one) type that conforms to TogleableItem but you want to use the view for a mix of types that conforms to TogleableItem.
The solution as I see it is to not make the view types generics and instead use TogleableItem directly in the declarations (I have skipped a lot of code below for brevity)
public struct ExternalView: View {
private let items: [any TogleableItem]
private let onItemTap: (Bool, Int, any TogleableItem) -> Void
...
}
struct InternalView<Content: View>: View {
private let items: [any TogleableItem]
private let content: (Int, any TogleableItem) -> Content
...
}
Another way to solve it if InternalView should be able to use other types than those conforming to TogleableItem is to use the original solution but without Element conforming to Hashable
struct InternalView<Element, Content: View>: View {
private let items: [Element]
private let content: (Int, Element) -> Content
I have a piece of sample code that shows picker. It is a simplified version of project that I'm working on. I have a view model that can be updated externally (via bluetooth - in example it's simulated) and by user using Picker. I would like to perform an action (for example an update) when user changes the value. I used onChange event on binding that is set on Picker and it works but the problem is that it also is called when value is changed externally which I don't want. Does anyone knows how to make it work as I expect?
enum Type {
case Type1, Type2
}
class ViewModel: ObservableObject {
#Published var type = Type.Type1
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.type = Type.Type2
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Row(type: $viewModel.type) {
self.update()
}
}
}
func update() {
// some update logic that should be called only when user changed value
print("update")
}
}
struct Row: View {
#Binding var type: Type
var action: () -> Void
var body: some View {
Picker("Type", selection: $type) {
Text("Type1").tag(Type.Type1)
Text("Type2").tag(Type.Type2)
}
.onChange(of: type, perform: { newType in
print("changed: \(newType)")
action()
})
}
}
EDIT:
I found a solution but I'm not sure if it's good one. I had to use custom binding like this:
struct Row: View {
#Binding var type: Type
var action: () -> Void
var body: some View {
let binding = Binding(
get: { self.type },
set: { self.type = $0
action()
}
)
return Picker("Type", selection: binding) {
Text("Type1").tag(Type.Type1)
Text("Type2").tag(Type.Type2)
}
}
}
There are several posts on how to pass a view to a struct using:
struct ContainerView<Content: View>: View {
let content: Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
content
}
}
But how do you pass a view as a parameter in a function?
You can actually pass a view as a generic:
func functionName<T:View>(viewYouArePassing: T){}
You also can use AnyView,
struct Whatever {
var view: AnyView
}
let myWhatever = Whatever(view: AnyView(CustomView))
My curiosity takes me to pass a View type as parameter to #ViewBuilder. Passing a Model/Primitive type as param in #ViewBuilder is perfectly valid.
As shown below code.
struct TestView<Content: View>: View {
let content: (String) -> Content
init(#ViewBuilder content: #escaping (String) -> Content) {
self.content = content
}
var body: some View {
content("Some text")
}
}
struct ContentTestView: View {
var body: some View {
TestView {
Text("\($0)")
}
}
}
In place of String in
let content: (String) -> Content
If I try to pass a SwiftUI View type, then Compiler is not happy with it.
let content: (View) -> Content
Even though params for #ViewBuilder accepts custom Protocol type like Searchable but not View protocol.
compiler tell me this Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
My whole idea is that content can be allowed to hold Section/List/Text in it.
Edit: I expected code like below.
struct TestView<Content: View>: View {
let content: (View) -> Content
init(#ViewBuilder content: #escaping (View) -> Content) {
self.content = content
}
var body: some View {
content(
List {
ForEach(0..<10) { i in
Text(\(i))
}
}
)
}
}
struct ContentTestView: View {
var body: some View {
TestView { viewbody -> _ in
Section(header: Text("Header goes here")) {
viewbody
}
}
}
}
Any way can I achieve this ?
The possible solution is to use AnyView, like
struct TestView<Content: View>: View {
let content: (AnyView) -> Content
init(#ViewBuilder content: #escaping (AnyView) -> Content) {
self.content = content
}
var body: some View {
content(AnyView(
Text("Demo") // << put here any view hierarchy
))
}
}
Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.