How to pass view as Parameter in a function Swift UI - ios

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

Related

Type any Protocol cannot conform to Protocol

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

How to implement Sheets in SwiftUI while using Generics and MVVM?

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?

SwiftUI, Passing a View as params in #Viewbuilder

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

SwiftUI NavigationLink loads destination view immediately, without clicking

With following code:
struct HomeView: View {
var body: some View {
NavigationView {
List(dataTypes) { dataType in
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
}
}
}
}
What's weird, when HomeView appears, NavigationLink immediately loads the AnotherView. As a result, all AnotherView dependencies are loaded as well, even though it's not visible on the screen yet. The user has to click on the row to make it appear.
My AnotherView contains a DataSource, where various things happen. The issue is that whole DataSource is loaded at this point, including some timers etc.
Am I doing something wrong..? How to handle it in such way, that AnotherView gets loaded once the user presses on that HomeViewRow?
The best way I have found to combat this issue is by using a Lazy View.
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Then the NavigationLink would look like this. You would place the View you want to be displayed inside ()
NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }
EDIT: See #MwcsMac's answer for a cleaner solution which wraps View creation inside a closure and only initializes it once the view is rendered.
It takes a custom ForEach to do what you are asking for since the function builder does have to evaluate the expression
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
for each visible row to be able to show HomeViewRow(dataType:), in which case AnotherView() must be initialized too.
So to avoid this a custom ForEach is necessary.
import SwiftUI
struct LoadLaterView: View {
var body: some View {
HomeView()
}
}
struct DataType: Identifiable {
let id = UUID()
var i: Int
}
struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
var data: Data
var destination: (Data.Element) -> (Destination)
var content: (Data.Element) -> (Content)
#State var selected: Data.Element? = nil
#State var active: Bool = false
var body: some View {
VStack{
NavigationLink(destination: {
VStack{
if self.selected != nil {
self.destination(self.selected!)
} else {
EmptyView()
}
}
}(), isActive: $active){
Text("Hidden navigation link")
.background(Color.orange)
.hidden()
}
List{
ForEach(data) { (element: Data.Element) in
Button(action: {
self.selected = element
self.active = true
}) { self.content(element) }
}
}
}
}
}
struct HomeView: View {
#State var dataTypes: [DataType] = {
return (0...99).map{
return DataType(i: $0)
}
}()
var body: some View {
NavigationView{
ForEachLazyNavigationLink(data: dataTypes, destination: {
return AnotherView(i: $0.i)
}, content: {
return HomeViewRow(dataType: $0)
})
}
}
}
struct HomeViewRow: View {
var dataType: DataType
var body: some View {
Text("Home View \(dataType.i)")
}
}
struct AnotherView: View {
init(i: Int) {
print("Init AnotherView \(i.description)")
self.i = i
}
var i: Int
var body: some View {
print("Loading AnotherView \(i.description)")
return Text("hello \(i.description)").onAppear {
print("onAppear AnotherView \(self.i.description)")
}
}
}
I had the same issue where I might have had a list of 50 items, that then loaded 50 views for the detail view that called an API (which resulted in 50 additional images being downloaded).
The answer for me was to use .onAppear to trigger all logic that needs to be executed when the view appears on screen (like setting off your timers).
struct AnotherView: View {
var body: some View {
VStack{
Text("Hello World!")
}.onAppear {
print("I only printed when the view appeared")
// trigger whatever you need to here instead of on init
}
}
}
For iOS 14 SwiftUI.
Non-elegant solution for lazy navigation destination loading, using view modifier, based on this post.
extension View {
func navigate<Value, Destination: View>(
item: Binding<Value?>,
#ViewBuilder content: #escaping (Value) -> Destination
) -> some View {
return self.modifier(Navigator(item: item, content: content))
}
}
private struct Navigator<Value, Destination: View>: ViewModifier {
let item: Binding<Value?>
let content: (Value) -> Destination
public func body(content: Content) -> some View {
content
.background(
NavigationLink(
destination: { () -> AnyView in
if let value = self.item.wrappedValue {
return AnyView(self.content(value))
} else {
return AnyView(EmptyView())
}
}(),
isActive: Binding<Bool>(
get: { self.item.wrappedValue != nil },
set: { newValue in
if newValue == false {
self.item.wrappedValue = nil
}
}
),
label: EmptyView.init
)
)
}
}
Call it like this:
struct ExampleView: View {
#State
private var date: Date? = nil
var body: some View {
VStack {
Text("Source view")
Button("Send", action: {
self.date = Date()
})
}
.navigate(
item: self.$date,
content: {
VStack {
Text("Destination view")
Text($0.debugDescription)
}
}
)
}
}
I was recently struggling with this issue (for a navigation row component for forms), and this did the trick for me:
#State private var shouldShowDestination = false
NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
Button("More info") {
self.shouldShowDestination = true
}
}
Simply wrap a Button with the NavigationLink, which activation is to be controlled with the button.
Now, if you're to have multiple button+links within the same view, and not an activation State property for each, you should rely on this initializer
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`.
public init<V>(destination: Destination, tag: V, selection: Binding<V?>, #ViewBuilder label: () -> Label) where V : Hashable
https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init
Along the lines of this example:
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
Button("Tap to show second") {
self.selection = "Second"
}
}
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
Button("Tap to show third") {
self.selection = "Third"
}
}
}
.navigationBarTitle("Navigation")
}
}
}
More info (and the slightly modified example above) taken from https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui (under "Programmatic navigation").
Alternatively, create a custom view component (with embedded NavigationLink), such as this one
struct FormNavigationRow<Destination: View>: View {
let title: String
let destination: Destination
var body: some View {
NavigationLink(destination: destination, isActive: $shouldShowDestination) {
Button(title) {
self.shouldShowDestination = true
}
}
}
// MARK: Private
#State private var shouldShowDestination = false
}
and use it repeatedly as part of a Form (or List):
Form {
FormNavigationRow(title: "One", destination: Text("1"))
FormNavigationRow(title: "Two", destination: Text("2"))
FormNavigationRow(title: "Three", destination: Text("3"))
}
In the destination view you should listen to the event onAppear and put there all code that needs to be executed only when the new screen appears. Like this:
struct DestinationView: View {
var body: some View {
Text("Hello world!")
.onAppear {
// Do something important here, like fetching data from REST API
// This code will only be executed when the view appears
}
}
}

SwiftUI View - viewDidLoad()?

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.

Resources