I am making an app with various screens, and I want to collect all the screens that all conform to View in an array of views. On the app's main screen, I want to show a list of all the screen titles with a NavigationLink to all of them.
The problem I am having is that if I try to create a custom view struct and have a property that initialises to a given screen and returns it. I keep running into issues and the compiler forces me to change the variable to accept any View rather than just View or the erased type some View.
struct Screen: View, Identifiable {
var id: String {
return title
}
let title: String
let destination: any View
var body: some View {
NavigationLink(destination: destination) { // Type 'any View' cannot conform to 'View'
Text(title)
}
}
}
This works:
struct Screen<Content: View>: View, Identifiable {
var id: String {
return title
}
let title: String
let destination: Content
var body: some View {
NavigationLink(destination: destination) {
Text(title)
}
}
}
But the issue with this approach is that I cannot put different screens into an Array which can be fixed as follows:
struct AllScreens {
var screens: [any View] = []
init(){
let testScreen = Screen(title: "Charties", destination: SwiftChartsScreen())
let testScreen2 = Screen(title: "Test", destination: NavStackScreen())
screens = [testScreen, testScreen2]
}
}
But when I try to access the screens in a List it cannot infer what view it is without typecasting. The end result I am trying to achieve is being able to pass in an array of screens and get their title displayed in a list like below.
At the moment I can only achieve this by hardcoding the lists elements, which works.
import SwiftUI
struct MainScreen: View {
let screens = AppScreens.allCases
var body: some View {
NavigationStack() {
List() {
AppScreens.chartsScreen
AppScreens.navStackScreen
}
.navigationTitle("WWDC 22")
}
}
}
SwiftUI is data-driven reactive framework and Swift is strict typed language, so instead of trying to put different View types (due to generics) into one array (requires same type), we can make data responsible for providing corresponding view (that now with help of ViewBuilder is very easy).
So here is an approach. Tested with Xcode 13+ / iOS 15+ (NavigationView or NavigationStack - it is not important)
enum AllScreens: CaseIterable, Identifiable { // << type !!
case charts, navStack // << known variants
var id: Self { self }
#ViewBuilder var view: some View { // corresponding view !!
switch self {
case .charts:
Screen(title: "Charties", destination: SwiftChartsScreen())
case .navStack:
Screen(title: "Test", destination: NavStackScreen())
}
}
}
usage is obvious:
struct MainScreen: View {
var body: some View {
NavigationStack { // or `NavigationView` for backward compatibility
List(AllScreens.allCases) {
$0.view // << data knows its presenter
}
.navigationTitle("WWDC 22")
}
}
}
Test module on GitHub
I have a LazyVStack which I would like to only update one view and not have all others on screen reload. With more complex cells this causes a big performance hit. I have included sample code
import SwiftUI
struct ContentView: View {
#State var items = [String]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(self.items, id: \.self) { item in
Button {
if let index = self.items.firstIndex(where: {$0 == item}) {
self.items[index] = "changed \(index)"
}
} label: {
cell(text: item)
}
}
}
}
.onAppear {
for _ in 0...200 {
self.items.append(NSUUID().uuidString)
}
}
}
}
struct cell: View {
let text: String
init(text: String) {
self.text = text
print("init cell", text)
}
var body: some View {
Text(text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you can see even when only changing 1 cell the init gets called for every cell. Is there anyway to avoid this?
Here is a working code, there is some points to mention, View in SwiftUI would get initialized here and there or anytime SwiftUI thinks it needed! But the body of View would get computed if really some value in body changed. It is planed to work like that, there is some exceptions as well. Like body get computed even the values that used in the body were as before with no change, I do not want inter to that topic! But in your example and in your issue, we want SwiftUI renders only the changed View, for this goal the down code works well without issue, but as you can see I used VStack, if we change VStack to LazyVStack, SwiftUI would renders some extra view due its undercover codes, and if you scroll to down and then to up, it would forget all rendered view and data in memory and it will try to render the old rendered views, so it is the nature of LazyVStack, we cannot do much about it. Apple want LazyVStack be Lazy. But you can see that LazyVStack would not render all views, but some of them that needs to works. we cannot say or know how much views get rendered in Lazy way, but for sure not all of them.
let initializingArray: () -> [String] = {
var items: [String] = [String]()
for _ in 0...200 { items.append(UUID().uuidString) }
return items
}
struct ContentView: View {
#State var items: [String] = initializingArray()
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Button(action: {
if let index = self.items.firstIndex(where: { $0 == item }) {
items[index] = "changed \(index)"
}
}, label: {
ItemView(item: item)
})
}
}
}
}
}
struct ItemView: View {
let item: String
var body: some View {
print("Rendering row for:", item)
return Text(item)
}
}
Goal: To use a common header View containing a shared title Text().
Scenario: I have multiple Views that share a common tab space within the one container tab View that contains a struct Header that is to be shared.
👉 This is a (many : 1) scenario.
Note: I don't want to use a NavigationView because it screws up landscape mode. A simple small header View is fine. I just need to populate the shared Title space amongst the member Views.
I don't want to merely add duplicate headers (having exactly the same layout) for each member View.
Several ideas: I need the header to respond to the 'change of title' event so I can see the new title.
So I believe I could use 1) #Binder(each member View) --> #State (shared Header View) or 2) #Environment.
I don't know how I could fit #1 into this particular scenario.
So I'm playing with #2: Environment Object.
DesignPattern: Main Header View's title set by multiple Views so the Header View is not aware of the multiple Views:
I'm not getting the EnvironmentObject paradigm to work.
Here's the codes...
MainView:
import SwiftUI
// Need to finish this.
class NYTEnvironment {
var title = "Title"
var msg = "Mother had a feeling..."
}
class NYTSettings: ObservableObject {
#Published var environment: NYTEnvironment
init() {
self.environment = NYTEnvironment()
}
}
struct NYTView: View {
var nytSettings = NYTSettings()
#State var selectionDataSegmentIndex = 0
var bindingDataSourceSegment: Binding<Int> {
.init(get: {
selectionDataSegmentIndex
}, set: {
selectionDataSegmentIndex = $0
})
}
var body: some View {
let county = 0; let state = 1; let states = 2
VStack {
NYTHeaderView()
SegmentAndDataPickerVStack(spacing: 10) {
if let segments = Source.NYT.dataSegments {
Picker("NYT Picker", selection: bindingDataSourceSegment) {
ForEach(segments.indices, id: \.self) { (index: Int) in
Text(segments[index])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
if selectionDataSegmentIndex == county {
NYTCountyView()
} else if selectionDataSegmentIndex == state {
NYTStateView()
} else if selectionDataSegmentIndex == states {
NYTStatesView()
}
Spacer()
}.environmentObject(nytSettings)
}
struct TrailingItem: View {
var body: some View {
Button(action: {
print("Info")
}, label: {
Image(systemName: "info.circle")
})
}
}
}
// ====================================================================================
struct NYTHeaderView: View {
#EnvironmentObject var nytSettings: NYTSettings
var body: some View {
ZStack {
Color.yellow
Text(nytSettings.environment.title)
}.frame(height: Header.navigationBarHeight)
}
}
Revision: I've added EnvironmentObject modifiers to the memberViews():
if selectionDataSegmentIndex == county {
NYTCountyView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == state {
NYTStateView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == states {
NYTStatesView().environmentObject(NYTSettings())
}
...
One of the member Views that's within the Main Container/Tab View (per above):
struct NYTCountyView: View {
#ObservedObject var dataSource = NYTCountyModel()
#EnvironmentObject var nytSettings: NYTSettings
...
...
}.onAppear {
nytSettings.environment.title = "Selected Counties"
if dataSource.revisedCountyElementListAndDuration == nil {
dataSource.getData()
}
}
Spacer()
...
}
Here's the compile-time error:
Modus Operandi: Set the title w/in header per member View upon .onAppear().
Problem: I'm not getting any title; just the default "Title" value.
Question: Am I on the right track? If so, what am I missing?
or... is there an alternative?
The whole problem boils down to a 'Many : 1' paradigm.
I got this revelation via taking a break and going for a walk.
So this is the proverbial 'round peg in a square hole' scenario.
What I needed was a lightly coupled relationship where the origin of the title value isn't required. Hence the use of the Notification paradigm.
The header view's title is the receiver and hence I used the .onReceive modifier:
struct NYTHeaderView: View {
#State private var title: String = ""
var body: some View {
ZStack {
Color.yellow
Text(title).onReceive(NotificationCenter.default.publisher(for: .headerTitle)) {note in
title = note.object as? String ?? "New York Times"
}
}.frame(height: Header.navigationBarHeight)
}
}
This sounds like what SwiftUI preferences was built to solve. The preferences are values collected and reduced from children for some distant ancestor to use. One notable example of this is how NavigationView gets its title - the title is set on the child, not on the NavigationView itself:
NavigationView {
Text("I am a simple view")
.navigationTitle("Title")
}
So, in your case you have some kind of title (simplified to String for brevity) that each child view might want to set. So you'd define a TitlePreferenceKey like so:
struct TitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
Here, the reduce function is simply applying the last value it sees from descendants, but since you'd only ever have one child view selected it should work.
Then, to use it, you'd have something like this:
struct NYTView: View {
#State var title = ""
#State var selection = 0
var body: some View {
VStack {
Text(title)
Picker("", selection: $selection) {
Text("SegmentA").tag(0)
Text("SegmentB").tag(1)
}
switch selection {
case 0: NYTCountyView()
case 1: NYTStateView()
.preference(key: TitlePreferenceKey.self, value: "State view")
default: EmptyView()
}
}
.onPreferenceChange(TitlePreferenceKey.self) {
self.title = $0
}
}
struct NYTCountyView: View {
#State var selectedCounty = "..."
var body: some View {
VStack {
//...
}
.preference(key: TitlePreferenceKey.self, value: selectedCounty)
}
}
So, a preference can be set by the parent of, as in the example of NYTStateView, or by the child with the value being dynamic, as in the example of NYTCountyView
I'm trying to figure out the correct way to conditionally include a view with swiftui. I wasn't able to use the if directly inside of a view and had to use a
stack view to do it.
This works but there seems like there would be a cleaner way.
var body: some View {
HStack() {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
The simplest way to avoid using an extra container like HStack is to annotate your body property as #ViewBuilder, like this:
#ViewBuilder
var body: some View {
if user.isLoggedIn {
MainView()
} else {
LoginView()
}
}
I needed to embed a view inside another conditionally, so I ended up creating a convenience if function:
extension View {
#ViewBuilder
func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
if conditional {
content(self)
} else {
self
}
}
}
This does return an AnyView, which is not ideal but feels like it is technically correct because you don't really know the result of this during compile time.
In my case, I needed to embed the view inside a ScrollView, so it looks like this:
var body: some View {
VStack() {
Text("Line 1")
Text("Line 2")
}
.if(someCondition) { content in
ScrollView(.vertical) { content }
}
}
But you could also use it to conditionally apply modifiers too:
var body: some View {
Text("Some text")
.if(someCondition) { content in
content.foregroundColor(.red)
}
}
UPDATE: Please read the drawbacks of using conditional modifiers before using this: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/
You didn't include it in your question but I guess the error you're getting when going without the stack is the following?
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
The error gives you a good hint of what's going on but in order to understand it, you need to understand the concept of opaque return types. That's how you call the types prefixed with the some keyword. I didn't see any Apple engineers going deep into that subject at WWDC (maybe I missed the respective talk?), which is why I did a lot of research myself and wrote an article on how these types work and why they are used as return types in SwiftUI.
🔗 What’s this “some” in SwiftUI?
There is also a detailed technical explanation in another
🔗 Stackoverflow post on opaque result types
If you want to fully understand what's going on I recommend reading both.
As a quick explanation here:
General Rule:
Functions or properties with an opaque result type (some Type)
must always return the same concrete type.
In your example, your body property returns a different type, depending on the condition:
var body: some View {
if someConditionIsTrue {
TabView()
} else {
LoginView()
}
}
If someConditionIsTrue, it would return a TabView, otherwise a LoginView. This violates the rule which is why the compiler complains.
If you wrap your condition in a stack view, the stack view will include the concrete types of both conditional branches in its own generic type:
HStack<ConditionalContent<TabView, LoginView>>
As a consequence, no matter which view is actually returned, the result type of the stack will always be the same and hence the compiler won't complain.
💡 Supplemental:
There is actually a view component SwiftUI provides specifically for this use case and it's actually what stacks use internally as you can see in the example above:
ConditionalContent
It has the following generic type, with the generic placeholder automatically being inferred from your implementation:
ConditionalContent<TrueContent, FalseContent>
I recommend using that view container rather that a stack because it makes its purpose semantically clear to other developers.
Anyway, the issue still exists.
Thinking mvvm-like all examples on that page breaks it.
Logic of UI contains in View.
In all cases is not possible to write unit-test to cover logic.
PS. I am still can't solve this.
UPDATE
I am ended with solution,
View file:
import SwiftUI
struct RootView: View {
#ObservedObject var viewModel: RatesListViewModel
var body: some View {
viewModel.makeView()
}
}
extension RatesListViewModel {
func makeView() -> AnyView {
if isShowingEmpty {
return AnyView(EmptyListView().environmentObject(self))
} else {
return AnyView(RatesListView().environmentObject(self))
}
}
}
Based on the comments I ended up going with this solution that will regenerate the view when the api key changes by using #EnvironmentObject.
UserData.swift
import SwiftUI
import Combine
import KeychainSwift
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
let keychain = KeychainSwift()
var apiKey : String? {
get {
keychain.get("api-key")
}
set {
if let newApiKey : String = newValue {
keychain.set(newApiKey, forKey: "api-key")
} else {
keychain.delete("api-key")
}
didChange.send(self)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView : View {
#EnvironmentObject var userData: UserData
var body: some View {
Group() {
if userData.apiKey != nil {
TabView()
} else {
LoginView()
}
}
}
}
Another approach using ViewBuilder (which relies on the mentioned ConditionalContent)
buildEither + optional
import PlaygroundSupport
import SwiftUI
var isOn: Bool?
struct TurnedOnView: View {
var body: some View {
Image(systemName: "circle.fill")
}
}
struct TurnedOffView: View {
var body: some View {
Image(systemName: "circle")
}
}
struct ContentView: View {
var body: some View {
ViewBuilder.buildBlock(
isOn == true ?
ViewBuilder.buildEither(first: TurnedOnView()) :
ViewBuilder.buildEither(second: TurnedOffView())
)
}
}
let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView
(There's also buildIf, but I couldn't figure out its syntax yet. ¯\_(ツ)_/¯)
One could also wrap the result View into AnyView
import PlaygroundSupport
import SwiftUI
let isOn: Bool = false
struct TurnedOnView: View {
var body: some View {
Image(systemName: "circle.fill")
}
}
struct TurnedOffView: View {
var body: some View {
Image(systemName: "circle")
}
}
struct ContentView: View {
var body: AnyView {
isOn ?
AnyView(TurnedOnView()) :
AnyView(TurnedOffView())
}
}
let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView
But it kinda feels wrong...
Both examples produce the same result:
I chose to solve this by creating a modifier that makes a view "visible" or "invisible". The implementation looks like the following:
import Foundation
import SwiftUI
public extension View {
/**
Returns a view that is visible or not visible based on `isVisible`.
*/
func visible(_ isVisible: Bool) -> some View {
modifier(VisibleModifier(isVisible: isVisible))
}
}
fileprivate struct VisibleModifier: ViewModifier {
let isVisible: Bool
func body(content: Content) -> some View {
Group {
if isVisible {
content
} else {
EmptyView()
}
}
}
}
Then to use it to solve your example, you would simply invert the isVisible value as seen here:
var body: some View {
HStack() {
TabView().visible(keychain.get("api-key") != nil)
LoginView().visible(keychain.get("api-key") == nil)
}
}
I have considered wrapping this into some kind of an "If" view that would
take two views, one when the condition is true and one when the condition is
false, but I decided that my present solution is both more general and more
readable.
Extension with the condition param works well for me (iOS 14):
import SwiftUI
extension View {
func showIf(condition: Bool) -> AnyView {
if condition {
return AnyView(self)
}
else {
return AnyView(EmptyView())
}
}
}
Example usage:
ScrollView { ... }.showIf(condition: shouldShow)
If the error message is
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
Just hide the complexity of the control flow from the ViewBuilder:
This works:
struct TestView: View {
func hiddenComplexControlflowExpression() -> Bool {
// complex condition goes here, like "if let" or "switch"
return true
}
var body: some View {
HStack() {
if hiddenComplexControlflowExpression() {
Text("Hello")
} else {
Image("test")
}
if hiddenComplexControlflowExpression() {
Text("Without else")
}
}
}
}
Previous answers were correct, however, I would like to mention, you may use optional views inside you HStacks. Lets say you have an optional data eg. the users address. You may insert the following code:
// works!!
userViewModel.user.address.map { Text($0) }
Instead of the other approach:
// same logic, won't work
if let address = userViewModel.user.address {
Text(address)
}
Since it would return an Optional text, the framework handles it fine. This also means, using an expression instead of the if statement is also fine, like:
// works!!!
keychain.get("api-key") != nil ? TabView() : LoginView()
In your case, the two can be combined:
keychain.get("api-key").map { _ in TabView() } ?? LoginView()
Using beta 4
I extended #gabriellanata's answer for up to two conditions. You can add more if needed. You use it like this:
Text("Hello")
.if(0 == 1) { $0 + Text("World") }
.elseIf(let: Int("!")?.description) { $0 + Text($1) }
.else { $0.bold() }
The code:
extension View {
func `if`<TrueContent>(_ condition: Bool, #ViewBuilder transform: #escaping (Self) -> TrueContent)
-> ConditionalWrapper1<Self, TrueContent> where TrueContent: View {
ConditionalWrapper1<Self, TrueContent>(content: { self },
conditional: Conditional<Self, TrueContent>(condition: condition,
transform: transform))
}
func `if`<TrueContent: View, Item>(`let` item: Item?, #ViewBuilder transform: #escaping (Self, Item) -> TrueContent)
-> ConditionalWrapper1<Self, TrueContent> {
if let item = item {
return self.if(true, transform: {
transform($0, item)
})
} else {
return self.if(false, transform: {
transform($0, item!)
})
}
}
}
struct Conditional<Content: View, Trans: View> {
let condition: Bool
let transform: (Content) -> Trans
}
struct ConditionalWrapper1<Content: View, Trans1: View>: View {
var content: () -> Content
var conditional: Conditional<Content, Trans1>
func elseIf<Trans2: View>(_ condition: Bool, #ViewBuilder transform: #escaping (Content) -> Trans2)
-> ConditionalWrapper2<Content, Trans1, Trans2> {
ConditionalWrapper2(content: content,
conditionals: (conditional,
Conditional(condition: condition,
transform: transform)))
}
func elseIf<Trans2: View, Item>(`let` item: Item?, #ViewBuilder transform: #escaping (Content, Item) -> Trans2)
-> ConditionalWrapper2<Content, Trans1, Trans2> {
let optionalConditional: Conditional<Content, Trans2>
if let item = item {
optionalConditional = Conditional(condition: true) {
transform($0, item)
}
} else {
optionalConditional = Conditional(condition: false) {
transform($0, item!)
}
}
return ConditionalWrapper2(content: content,
conditionals: (conditional, optionalConditional))
}
func `else`<ElseContent: View>(#ViewBuilder elseTransform: #escaping (Content) -> ElseContent)
-> ConditionalWrapper2<Content, Trans1, ElseContent> {
ConditionalWrapper2(content: content,
conditionals: (conditional,
Conditional(condition: !conditional.condition,
transform: elseTransform)))
}
var body: some View {
Group {
if conditional.condition {
conditional.transform(content())
} else {
content()
}
}
}
}
struct ConditionalWrapper2<Content: View, Trans1: View, Trans2: View>: View {
var content: () -> Content
var conditionals: (Conditional<Content, Trans1>, Conditional<Content, Trans2>)
func `else`<ElseContent: View>(#ViewBuilder elseTransform: (Content) -> ElseContent) -> some View {
Group {
if conditionals.0.condition {
conditionals.0.transform(content())
} else if conditionals.1.condition {
conditionals.1.transform(content())
} else {
elseTransform(content())
}
}
}
var body: some View {
self.else { $0 }
}
}
How about that?
I have a conditional contentView, which either is a text or an icon. I solved the problem like this. Comments are very appreciated, since I don't know if this is really "swifty" or just a "hack", but it works:
private var contentView : some View {
switch kind {
case .text(let text):
let textView = Text(text)
.font(.body)
.minimumScaleFactor(0.5)
.padding(8)
.frame(height: contentViewHeight)
return AnyView(textView)
case .icon(let iconName):
let iconView = Image(systemName: iconName)
.font(.title)
.frame(height: contentViewHeight)
return AnyView(iconView)
}
}
Use Group instead of HStack
var body: some View {
Group {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
Here’s a very simple to use modifier which uses a boolean test to decide if a view will be rendered. Unlike other solutions posted here it doesn’t rely on the use of ÀnyView. This is how to use it:
var body: some View {
VStack {
FooView()
.onlyIf(someCondition)
}
}
This reads nicer than the default if … then construct as it removes the additional indentation.
To replace an if … then … else construct, this is the obvious solution:
var body: some View {
VStack {
FooView()
.onlyIf(someCondition)
BarView()
.onlyIf(!someCondition)
}
}
This is the definition of the onlyIf modifier:
struct OnlyIfModifier: ViewModifier {
var condition: Bool
func body(content: Content) -> some View {
if condition {
content
}
}
}
extension View {
func onlyIf(_ condition: Bool) -> some View {
modifier(OnlyIfModifier(condition: condition))
}
}
Give it a try – it will surely clean up your code and improve overall readability.
If you want to navigate to two different views using NavigationLink, you can navigate using ternary operator.
let profileView = ProfileView()
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
let otherProfileView = OtherProfileView(data: user)
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
NavigationLink(destination: profileViewModel.userName == user.userName ? AnyView(profileView) : AnyView(otherProfileView)) {
HStack {
Text("Navigate")
}
}
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.