I am trying to learn SwiftUI and creating a movie search app with The movie Database API
I would like to fetch new data once the scroll goes at the end of the List. I found a possible solution on SO using ForEach inside the List and checking when the list reaches the last item and then performing the onAppear() call.
In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data
How can I load new pages from the search when the first page has loaded?
You can find the project here, was too big to post
https://github.com/aspnet82/MovieSearch
SearchMovieManager -> Make the fetch call, I used Dispatch.main.async
Here what the app should do
Fetch the data from the API and display the first page -> WORKING
When the List scroll to the last item makes another fetch request on the following page to load more data -> This not works, it works only for the second page of the request. It always displays the same data after the second fetch call.
The issue I think is in the GCD, but I do not know how to queue things to make it works automatically
UPDATE: A workaround I found:
List(searchMovieManager.allMovies) { movie in
Text(movie.title)
}
Text("load more").onTapGesture {
fetchNextPage(obs: self.searchMovieManager, page: self.searchMovieManager.pageTofetch)
}
I think might be ok as solution, it adds new page once I tap on the button, and can be also fine in controlling data download maybe?
Thanks for the help :)
Try the next, it use anchor preferences and simple model which mimics async operation to add some records to ScrollView (or List)
import SwiftUI
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
struct Data: Identifiable {
let id: Int
}
class Model: ObservableObject {
var _flag = false
var flag: Bool {
get {
_flag
}
set(newValue) {
if newValue == true {
_flag = newValue
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self._flag = false
self.rows += 20
print("done")
}
}
}
}
#Published var rows = 20
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
List {
ForEach(0 ..< model.rows, id:\.self) { i in
Text("row \(i)").font(.largeTitle).tag(i)
}
Rectangle().tag(model.rows).frame(height: 0).anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.model.rows, center: anchor)]
}.id(model.rows)
}
.backgroundPreferenceValue(Positions.self) { (preferences) in
GeometryReader { proxy in
Rectangle().frame(width: 0, height: 0).position(self.getPosition(proxy: proxy, tag: self.model.rows, preferences: preferences))
}
}
}
func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
let p = preferences.filter({ (p) -> Bool in
p.id == tag
})
if p.isEmpty { return .zero }
if proxy.size.height - proxy[p[0].center].y > 0 && model.flag == false {
self.model.flag.toggle()
print("fetch")
}
return .zero
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and here is how it looks like ...
Related
I was wondering if someone can help me solve this problem. I have the following rating view:
import SwiftUI
struct RatingView: View {
#Binding var rating: Int
var label = ""
var maxRating = 5
var offImage: Image?
var onImage = Image(systemName: "star.fill")
var offColor = Color.gray
var onColor = Color.yellow
var body: some View {
HStack{
if label.isEmpty == false {
Text(label)
}
ForEach(1..<maxRating + 1, id: \.self){ number in
image(for: number)
.foregroundColor(number > rating ? offColor : onColor)
.onTapGesture {
rating = number
}
}
}
}
func image(for number: Int) -> Image {
if number > rating {
return offImage ?? onImage
} else {
return onImage
}
}
}
struct RatingView_Previews: PreviewProvider {
static var previews: some View {
RatingView(rating: .constant(4))
}
}
I wanted to have a custom rating for each image displayed in the ContenView, which is as follows:
import SwiftUI
struct ContentView: View {
var pictureURL = DbHelper().getPictures()
#EnvironmentObject var authenticator: Authenticator
#State private var rating: Int = 0
var body: some View {
List(pictureURL, id: \.self) { photo in
VStack{
if #available(iOS 15.0, *) {
AsyncImage(url: URL(string: photo), scale: 10.0)
} else {
// Fallback on earlier versions
}
RatingView(rating: $rating)
}
}
Button("Logout") {
authenticator.logout()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Authenticator())
}
}
This code displays the view but when I click on a start, all of the ratings change to that value. I eventually want to add to a database, for specified user who is logged in, the rating, and image URL associated with that rating.
Any hints on how to go about this would be helpful.
Within your loop, all of your rating views bind to the same $rating state variable. So, if you have five images and ratings views for each, if you tap on the first one, that changes ContentView's rating value, and that change is then reflected in the other four.
To have individual ratings, you'll need some way of associating a rating with a picture URL. Depending on how you end up modelling your data, that could involve replacing pictureURL, an array of URL strings, with an array of objects, each of which contains the URL and a bindable rating that applies to that user.
There are other ways of structuring that data, depending on how your final model needs to be structured, but the key will always be that the rating you pass to your RatingView will need to apply to the current URL and no others.
I have a list of sliders, but I have a problem updating the text that shows the slider value.
The app workflow is like this:
User taps to add a new slider to the list.
An object that defines the slider is created and stored in an array.
The class that has the array as a property (Db) is an ObservableObject and triggers a View update for each new item.
The list is updated with a new row.
So far, so good. Each row has a slider whose value is stored in a property in an object in an array. However, the value text doesn't update as soon as the slider is moved, but when a new item is added. Please see the GIF below:
The Slider doesn't update the text value when moved
How can I bind the slider movements to the text value? I thought that by defining
#ObservedObject var slider_value: SliderVal = SliderVal()
and binding that variable to the slider, the value would be updated simultaneously but that is not the case. Thanks a lot for any help.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var db: Db
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value)) //<-- Problem here
}
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
struct Criteria: Identifiable {
var id = UUID()
var name: String
#ObservedObject var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
class SliderVal: ObservableObject {
#Published var value:Double = 50
}
The #ObservableObject won't work within a struct like that -- it's only useful inside a SwiftUI View or a DynamicProperty. With your use case, because the class is a reference type, the #Published property has no way of knowing that the SliderVal was changed, so the owner View never gets updated.
You can fix this by turning your model into a struct:
struct Criteria: Identifiable {
var id = UUID()
var name: String
var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
struct SliderVal {
var value:Double = 50
}
The problem, once you do this, is you don't have a Binding to use in your List. If you're lucky enough to be on SwiftUI 3.0 (iOS 15 or macOS 12), you can use $criteria within your list to get a binding to the element being currently iterated over.
If you're on an earlier version, you'll need to either use indexes to iterate over the items, or, my favorite, create a custom binding that is tied to the id of the item. It looks like this:
struct ContentView: View {
#ObservedObject var db: Db = Db()
private func bindingForId(id: UUID) -> Binding<Criteria> {
.init {
db.criteria_db.first { $0.id == id } ?? Criteria(name: "")
} set: { newValue in
db.criteria_db = db.criteria_db.map {
$0.id == id ? newValue : $0
}
}
}
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value))
}
Slider(value: bindingForId(id: criteria.id).slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
Now, because the models are all value types (structs), the View and #Published know when to update and your sliders work as expected.
try something like this:
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
.onChange(of: criteria.slider_value.value) { newVal in
DispatchQueue.main.async {
criteria.slider_value.value = newVal
}
}
Q1: Why are onAppears called twice?
Q2: Alternatively, where can I make my network call?
I have placed onAppears at a few different place in my code and they are all called twice. Ultimately, I'm trying to make a network call before displaying the next view so if you know of a way to do that without using onAppear, I'm all ears.
I have also tried to place and remove a ForEach inside my Lists and it doesn't change anything.
Xcode 12 Beta 3 -> Target iOs 14
CoreData enabled but not used yet
struct ChannelListView: View {
#EnvironmentObject var channelStore: ChannelStore
#State private var searchText = ""
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List() {
ForEach(channelStore.allChannels) { channel in
NavigationLink(destination: VideoListView(channel: channel)
.onAppear(perform: {
print("PREVIOUS VIEW ON APPEAR")
})) {
ChannelRowView(channel: channel)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
struct VideoListView: View {
#EnvironmentObject var videoStore: VideoStore
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var channel: Channel
var body: some View {
List(videoStore.allVideos) { video in
VideoRowView(video: video)
}
.onAppear(perform: {
print("LIST ON APPEAR")
})
.navigationTitle("Videos")
.navigationBarItems(trailing: Button(action: {
networking.getTopVideos(channelID: channel.channelId) { (videos) in
var videoIdArray = [String]()
videoStore.allVideos = videos
for video in videoStore.allVideos {
videoIdArray.append(video.videoID)
}
for (index, var video) in videoStore.allVideos.enumerated() {
networking.getViewCount(videoID: videoIdArray[index]) { (viewCount) in
video.viewCount = viewCount
videoStore.allVideos[index] = video
networking.setVideoThumbnail(video: video) { (image) in
video.thumbnailImage = image
videoStore.allVideos[index] = video
}
}
}
}
}) {
Text("Button")
})
.onAppear(perform: {
print("BOTTOM ON APPEAR")
})
}
}
I had the same exact issue.
What I did was the following:
struct ContentView: View {
#State var didAppear = false
#State var appearCount = 0
var body: some View {
Text("Appeared Count: \(appearrCount)"
.onAppear(perform: onLoad)
}
func onLoad() {
if !didAppear {
appearCount += 1
//This is where I loaded my coreData information into normal arrays
}
didAppear = true
}
}
This solves it by making sure only what's inside the the if conditional inside of onLoad() will run once.
Update: Someone on the Apple Developer forums has filed a ticket and Apple is aware of the issue. My solution is a temporary hack until Apple addresses the problem.
I've been using something like this
import SwiftUI
struct OnFirstAppearModifier: ViewModifier {
let perform:() -> Void
#State private var firstTime: Bool = true
func body(content: Content) -> some View {
content
.onAppear{
if firstTime{
firstTime = false
self.perform()
}
}
}
}
extension View {
func onFirstAppear( perform: #escaping () -> Void ) -> some View {
return self.modifier(OnFirstAppearModifier(perform: perform))
}
}
and I use it instead of .onAppear()
.onFirstAppear{
self.vm.fetchData()
}
you can create a bool variable to check if first appear
struct VideoListView: View {
#State var firstAppear: Bool = true
var body: some View {
List {
Text("")
}
.onAppear(perform: {
if !self.firstAppear { return }
print("BOTTOM ON APPEAR")
self.firstAppear = false
})
}
}
Let us assume you are now designing a SwiftUI and your PM is also a physicist and philosopher. One day he tells you we should to unify UIView and UIViewController, like Quantum Mechanics and the Theory of Relativity. OK, you are like-minded with your leader, voting for "Simplicity is Tao", and create an atom named "View". Now you say: "View is everything, view is all". That sounds awesome and seems feasible. Well, you commit the code and tell the PM….
onAppear and onDisAppear exists in every view, but what you really need is a Page lifecycle callback. If you use onAppear like viewDidAppear, then you get two problems:
Being influenced by the parent, the child view will rebuild more than one time, causing onAppear to be called many times.
SwiftUI is closed source, but you should know this: view = f(view). So, onAppear will run to return a new View, which is why onAppear is called twice.
I want to tell you onAppear is right! You MUST CHANGE YOUR IDEAS. Don’t run lifecycle code in onAppear and onDisAppear! You should run that code in the "Behavior area". For example, in a button navigating to a new page.
You can create the first appear function for this bug
extension View {
/// Fix the SwiftUI bug for onAppear twice in subviews
/// - Parameters:
/// - perform: perform the action when appear
func onFirstAppear(perform: #escaping () -> Void) -> some View {
let kAppearAction = "appear_action"
let queue = OperationQueue.main
let delayOperation = BlockOperation {
Thread.sleep(forTimeInterval: 0.001)
}
let appearOperation = BlockOperation {
perform()
}
appearOperation.name = kAppearAction
appearOperation.addDependency(delayOperation)
return onAppear {
if !delayOperation.isFinished, !delayOperation.isExecuting {
queue.addOperation(delayOperation)
}
if !appearOperation.isFinished, !appearOperation.isExecuting {
queue.addOperation(appearOperation)
}
}
.onDisappear {
queue.operations
.first { $0.name == kAppearAction }?
.cancel()
}
}
}
For everyone still having this issue and using a NavigationView. Add this line to the root NavigationView() and it should fix the problem.
.navigationViewStyle(StackNavigationViewStyle())
From everything I have tried, this is the only thing that worked.
We don't have to do it on .onAppear(perform)
This can be done on init of View
In case someone else is in my boat, here is how I solved it for now:
struct ChannelListView: View {
#State private var searchText = ""
#State private var isNavLinkActive: Bool = false
#EnvironmentObject var channelStore: ChannelStore
#ObservedObject private var networking = Networking()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
.padding(.top, 20)
List(channelStore.allChannels) { channel in
ZStack {
NavigationLink(destination: VideoListView(channel: channel)) {
ChannelRowView(channel: channel)
}
HStack {
Spacer()
Button {
isNavLinkActive = true
// Place action/network call here
} label: {
Image(systemName: "arrow.right")
}
.foregroundColor(.gray)
}
}
.listStyle(GroupedListStyle())
}
.navigationTitle("Channels")
}
}
}
}
I've got this app:
#main
struct StoriesApp: App {
var body: some Scene {
WindowGroup {
TabView {
NavigationView {
StoriesView()
}
}
}
}
}
And here is my StoriesView:
// ISSUE
struct StoriesView: View {
#State var items: [Int] = []
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
///////////////////////////////////
// Gets called 2 times on app start <--------
///////////////////////////////////
}
}
I've resolved the issue by measuring the diff time between onAppear() calls. According to my observations double calls of onAppear() happen between 0.02 and 0.45 seconds:
// SOLUTION
struct StoriesView: View {
#State var items: [Int] = []
#State private var didAppearTimeInterval: TimeInterval = 0
var body: some View {
List {
ForEach(items, id: \.self) { id in
StoryCellView(id: id)
}
}
.onAppear(perform: onAppear)
}
private func onAppear() {
if Date().timeIntervalSince1970 - didAppearTimeInterval > 0.5 {
///////////////////////////////////////
// Gets called only once in 0.5 seconds <-----------
///////////////////////////////////////
}
didAppearTimeInterval = Date().timeIntervalSince1970
}
}
In my case, I found that a few views up the hierarchy, .onAppear() (and .onDisappear()) was only being called once, as expected. I used that to post notifications that I listen to down in the views that need to take action on those events. It’s a gross hack, and I’ve verified that the bug is fixed in iOS 15b1, but Apple really needs to backport the fix.
I've been writing my first SwiftUI application, which manages a book collection. It has a List of around 3,000 items, which loads and scrolls pretty efficiently. If use a toggle control to filter the list to show only the books I don't have the UI freezes for twenty to thirty seconds before updating, presumably because the UI thread is busy deciding whether to show each of the 3,000 cells or not.
Is there a good way to do handle updates to big lists like this in SwiftUI?
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
ForEach(userData.bookList) { book in
if !self.userData.showWantsOnly || !book.own {
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
}.navigationBarTitle(Text("Books"))
}
Have you tried passing a filtered array to the ForEach. Something like this:
ForEach(userData.bookList.filter { return !$0.own }) { book in
NavigationLink(destination: BookDetail(book: book)) { BookRow(book: book) }
}
Update
As it turns out, it is indeed an ugly, ugly bug:
Instead of filtering the array, I just remove the ForEach all together when the switch is flipped, and replace it by a simple Text("Nothing") view. The result is the same, it takes 30 secs to do so!
struct SwiftUIView: View {
#EnvironmentObject var userData: UserData
#State private var show = false
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
if self.userData.showWantsOnly {
Text("Nothing")
} else {
ForEach(userData.bookList) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
}.navigationBarTitle(Text("Books"))
}
}
Workaround
I did find a workaround that works fast, but it requires some code refactoring. The "magic" happens by encapsulation. The workaround forces SwiftUI to discard the List completely, instead of removing one row at a time. It does so by using two separate lists in two separate encapsualted views: Filtered and NotFiltered. Below is a full demo with 3000 rows.
import SwiftUI
class UserData: ObservableObject {
#Published var showWantsOnly = false
#Published var bookList: [Book] = []
init() {
for _ in 0..<3001 {
bookList.append(Book())
}
}
}
struct SwiftUIView: View {
#EnvironmentObject var userData: UserData
#State private var show = false
var body: some View {
NavigationView {
VStack {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
if userData.showWantsOnly {
Filtered()
} else {
NotFiltered()
}
}
}.navigationBarTitle(Text("Books"))
}
}
struct Filtered: View {
#EnvironmentObject var userData: UserData
var body: some View {
List(userData.bookList.filter { $0.own }) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
struct NotFiltered: View {
#EnvironmentObject var userData: UserData
var body: some View {
List(userData.bookList) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
struct Book: Identifiable {
let id = UUID()
let own = Bool.random()
}
struct BookRow: View {
let book: Book
var body: some View {
Text("\(String(book.own)) \(book.id)")
}
}
struct BookDetail: View {
let book: Book
var body: some View {
Text("Detail for \(book.id)")
}
}
Check this article https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
In short the solution proposed in this article is to add .id(UUID()) to the list:
List(items, id: \.self) {
Text("Item \($0)")
}
.id(UUID())
"Now, there is a downside to using id() like this: you won't get your update animated. Remember, we're effectively telling SwiftUI the old list has gone away and there's a new list now, which means it won't try to move rows around in an animated way."
I think we have to wait until SwiftUI List performance improves in subsequent beta releases. I’ve experienced the same lag when lists are filtered from a very large array (500+) down to very small ones. I created a simple test app to time the layout for a simple array with integer IDs and strings with Buttons to simply change which array is being rendered - same lag.
Instead of a complicated workaround, just empty the List array and then set the new filters array. It may be necessary to introduce a delay so that emptying the listArray won't be omitted by the followed write.
List(listArray){item in
...
}
self.listArray = []
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.listArray = newList
}
Looking for how to adapt Seitenwerk's response to my solution, I found a Binding extension that helped me a lot. Here is the code:
struct ContactsView: View {
#State var stext : String = ""
#State var users : [MockUser] = []
#State var filtered : [MockUser] = []
var body: some View {
Form{
SearchBar(text: $stext.didSet(execute: { (response) in
if response != "" {
self.filtered = []
self.filtered = self.users.filter{$0.name.lowercased().hasPrefix(response.lowercased()) || response == ""}
}
else {
self.filtered = self.users
}
}), placeholder: "Buscar Contactos")
List{
ForEach(filtered, id: \.id){ user in
NavigationLink(destination: LazyView( DetailView(user: user) )) {
ContactCell(user: user)
}
}
}
}
.onAppear {
self.users = LoadUserData()
self.filtered = self.users
}
}
}
This is the Binding extension:
extension Binding {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print($0) }, in: 0...10)
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
The LazyView is optional, but I took the trouble to show it, as it helps a lot in the performance of the list, and prevents swiftUI from creating the NavigationLink target content of the whole list.
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
This code will work correctly provided that you initialize your class in the 'SceneDelegate' file as follows:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var userData = UserData()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView:
contentView
.environmentObject(userData)
)
self.window = window
window.makeKeyAndVisible()
}
}
So I tried to put a print statement while debugging in a SwiftUI View.
print("landmark: \(landmark)")
In the following body.
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Favorite only")
}
ForEach(landmarkData) { landmark in
print("landmark: \(landmark)")
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
Compiler errors out:
So, what is the proper way to print to console in SwiftUI?
EDIT:
I made Landmark conform to CustomStringConvertible:
struct Landmark: Hashable, Codable, Identifiable, CustomStringConvertible {
var description: String { name+"\(id)" }
var id: Int
var name: String
.....
I still get the "String is not convertible to any" error. Should it work now?
You can easily add a print statement anywhere in a function builder by simply storing its return value in a wildcard, effectively ignoring it:
let _ = print("hi!")
No setup or other verbosity needed!
Why does this work while a regular print() doesn't?
The way SwiftUI's #ViewBuilder (and result builders in general) is that they consume any values in a closure that aren't used otherwise (e.g. if you just have 42 on its own line). The print function returns Void (nothing), which the builder would have to build into a view, so it fails. By instead assigning it to a variable (in this case _, basically a variable that you can never access), the Void is never offered to the view builder in the first place.
You could argue the builder should simply accept and ignore Void values, but the idea is that your builder closures should not have side effects (I'd remove print statements after finishing debugging too)—you should not rely on these closures being called at certain times.
Here's a helper Print( ... ) View that acts like a print( ... ) function but within a View
Put this in any of your view files
extension View {
func Print(_ vars: Any...) -> some View {
for v in vars { print(v) }
return EmptyView()
}
}
and use inside of body like so
Print("Here I am", varOne, varTwo ...)
or inside a ForEach {} like so
self.Print("Inside ForEach", varOne, varTwo ...)
Note: you might need to put Print() into a Group {} when combining with existing views
Try right-clicking on the live preview play button and selecting 'Debug Preview from the popup
You can print in the body structure but to do so you have to explicitly return the view you want to render. The body property inside a View is just a computed property like any other in Swift that implicitly returns the view. And just like any other computed property, you can perform operations inside the computed property as long as a value is explicitly returned. For example, this will throw an error when you try to print because there is no explicit return:
struct SomeView: View {
#State var isOpen = false
var body: some View {
print(isOpen) // error thrown here
VStack {
// other view code
}
}
}
But if we explicitly return the view we want then it will work e.g.
struct SomeView: View {
#State var isOpen = false
var body: some View {
print(isOpen) // this ok because we explicitly returned the view below
// Notice the added 'return' below
return VStack {
// other view code
}
}
}
The above will work well if you're looking to view how state or environment objects are changing before returning your view, but if you want to print something deeper down within the view you are trying to return, then I would go with #Rok Krulec answer.
It is possible to use print() remembering that all SwiftUI View content are (a) implicit closures and (b) it is highly recommended to decompose views as much as possible to have simple structure, so it might look like the following...
struct Model: Identifiable {
let value: String
var id: String {
value
}
init (_ value: String) {
self.value = value
}
}
struct TestView: View {
#State var showFavoritesOnly = false
#State var listData: [Model] = [Model("one"), Model("two"), Model("three")]
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorite only")
}
ForEach(listData) { data in
self.rowView(data: data)
}
}
}
}
private func rowView(data: Model) -> some View {
#if DEBUG
print(">> \(data.value)")
#endif
return NavigationLink(destination: Text("Details")) {
Text("Go next from \(data.value)")
}
}
}
... and right clicking in Preview to select run as Debug Preview we get:
2019-10-31 14:28:03.467635+0200 Test[65344:11155167] [Agent] Received connection, creating agent
2019-10-31 14:28:04.472314+0200 Test[65344:11155168] [Agent] Received display message
>> one
>> two
>> three
You can declare a printing() method that includes print() and returns EmptyView struct.
struct ContentView: View {
#State private var offset = CGSize.zero
func printing(_ items: Any...) -> some View {
let _ = print(items)
return EmptyView()
}
var body: some View {
#if DEBUG
printing(offset) // prints [(0.0, 0.0)]
#endif
ZStack {
Text("Hello")
}
}
}
The safest and easiest way to print while debugging in a SwiftUI View.
extension View {
func Print(_ item: Any) -> some View {
#if DEBUG
print(item)
#endif
return self
}
}
Usage Example:
struct ContentView: View {
var body: some View {
VStack {
ForEach((1...5), id: \.self) { number in
Text("\(number)")
.Print(number)
}
}
}
}
Console output:
1
2
3
4
5
It can be generalized to:
extension View {
func Perform(_ block: () -> Void) -> some View {
block()
return EmptyView()
}
}
So in your example:
ForEach(landmarkData) { landmark in
Perform { print("landmark: \(landmark)") }
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
Here you go. It will just work like simple print but inside a view.
func printv( _ data : Any)-> EmptyView{
print(data)
return EmptyView()
}
and use it like that
struct ContentView: View {
var body: some View {
VStack() {
Text("hello To SwiftUI")
printv("its easy to code in SwiftUI")
Text("And Good to have you here")
}
}
}
The following extension on View is as intuitive as print because it's made to replicate the default print(_:separator:terminator:) function signature & behavior.
extension View {
func printUI(_ args: Any..., separator: String = " ", terminator: String = "\n") -> EmptyView {
let output = args.map(String.init(describing:)).joined(separator: separator)
print(output, terminator: terminator)
return EmptyView()
}
}
Usage Example:
struct ContentView: View {
var body: some View {
VStack {
printUI("ContentView", "1")
printUI("ContentView", "2", separator: ", ", terminator: "\n.\n.\n")
printUI("ContentView", "3", separator: "; ")
Text("Hello, World!")
}
}
}
Console Output:
ContentView 1
ContentView, 2
.
.
ContentView; 3
EDIT: Debug Preview is no longer supported in the latest versions of Xcode.
Very easy way to debug your Preview:
Open your Swift project in Xcode 11.
Right-click (or Control-click) on the Live Preview button in the bottom right corner of the preview.
Select Debug Preview.
How to debug your SwiftUI previews in Xcode
// Try this, add a 'return' on a view then the 'print' can stay alive in.
struct ContentView: View {
var num: Int = 1
var body: some View {
print(num)
return Text("hello")
}
}
You can't because you're in a computed property. You need for example a button and in the action you define the print. Or work with breakpoints
You can not print in body structure i.e. a structure which is some view type.For print you need to make function out of body structure and call it using button or something else.
This should work
if true {
print(aVar, "xx")
}
return ZStack {
...
}